Add Audio to FFmpegNativeEncoder (Not Verified)

This commit is contained in:
copecone committed 2026-07-06 14:24:15 +09:00
1 parent d79ff3333a
commit 6cb8f29cc7
1 file changed
+474 -10
+474 -10
View File
@@ -1,4 +1,5 @@
using System; using System;
using System.Runtime.InteropServices;
using FFmpeg.AutoGen; using FFmpeg.AutoGen;
using MelonLoader; using MelonLoader;
using Unity.Collections; using Unity.Collections;
@@ -40,6 +41,36 @@ namespace ADOFAIRenderer.Util.FFmpeg
/// <summary> /// <summary>
/// 인코더에 전달할 설정값을 담는 구조체입니다. /// 인코더에 전달할 설정값을 담는 구조체입니다.
/// </summary> /// </summary>
/// <summary>
/// 오디오 인코딩에 전달할 설정값을 담는 구조체입니다.
/// </summary>
public struct AudioSettings
{
/// <summary>입력 오디오의 샘플 레이트 (Hz). 예: 44100, 48000.</summary>
public int SampleRate;
/// <summary>입력 오디오의 채널 수. 예: 1(모노), 2(스테레오).</summary>
public int Channels;
/// <summary>
/// 입력 오디오의 샘플 포맷. 기본값은 AV_SAMPLE_FMT_FLT (32-bit float).
/// Unity의 AudioClip.GetData()는 float[] 형태이므로 FLT가 적합합니다.
/// </summary>
public AVSampleFormat InputSampleFormat;
/// <summary>
/// 사용할 오디오 코덱 ID. 기본값(0)이면 AAC를 사용합니다.
/// 예: AV_CODEC_ID_AAC, AV_CODEC_ID_FLAC, AV_CODEC_ID_OPUS 등.
/// </summary>
public AVCodecID AudioCodecId;
/// <summary>
/// 오디오 비트레이트 (bps 단위). 기본값(0)이면 128000 bps를 사용합니다.
/// AAC의 경우 128000~320000 bps가 일반적입니다.
/// </summary>
public int Bitrate;
}
public struct EncoderSettings public struct EncoderSettings
{ {
/// <summary>출력 비디오의 가로 해상도 (픽셀).</summary> /// <summary>출력 비디오의 가로 해상도 (픽셀).</summary>
@@ -71,6 +102,12 @@ namespace ADOFAIRenderer.Util.FFmpeg
/// <summary>소프트웨어 CPU 인코더를 강제 사용할지 여부.</summary> /// <summary>소프트웨어 CPU 인코더를 강제 사용할지 여부.</summary>
public bool ForceSoftware; public bool ForceSoftware;
/// <summary>
/// 오디오 인코딩 설정. null이면 오디오 스트림 없이 비디오만 인코딩합니다 (기존 동작과 동일).
/// 값을 설정하면 비디오와 오디오가 함께 먹싱(muxing)됩니다.
/// </summary>
public AudioSettings? Audio;
} }
// ────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────
@@ -97,7 +134,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
/// 여기서는 비디오 스트림 하나만 사용합니다. /// 여기서는 비디오 스트림 하나만 사용합니다.
/// 참고: https://ffmpeg.org/doxygen/trunk/group__lavf__core.html /// 참고: https://ffmpeg.org/doxygen/trunk/group__lavf__core.html
/// </summary> /// </summary>
private AVStream* _pStream = null; private AVStream* _pVideoStream = null;
// ────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────
// 프레임 버퍼 및 색공간 변환기(스케일러) 포인터들 // 프레임 버퍼 및 색공간 변환기(스케일러) 포인터들
@@ -174,6 +211,44 @@ namespace ADOFAIRenderer.Util.FFmpeg
private AVFrame* _pHwFrame = null; private AVFrame* _pHwFrame = null;
private bool _loggedConversionMode; private bool _loggedConversionMode;
// ──────────────────────────────────────────────────────────────
// 오디오 인코딩 관련 FFmpeg 구조체 포인터들
// ──────────────────────────────────────────────────────────────
/// <summary>
/// 오디오 코덱(인코더)의 상태를 관리하는 컨텍스트.
/// 샘플 레이트, 채널 레이아웃, 샘플 포맷 등 오디오 인코딩 파라미터가 설정됩니다.
/// </summary>
private AVCodecContext* _pAudioCodecContext = null;
/// <summary>출력 파일 내의 오디오 스트림.</summary>
private AVStream* _pAudioStream = null;
/// <summary>오디오 인코딩된 패킷 데이터를 임시로 담을 패킷 구조체.</summary>
private AVPacket* _pAudioPacket = null;
/// <summary>
/// 오디오 리샘플링 컨텍스트 (SwrContext).
/// 입력 PCM 포맷(예: float interleaved) → 인코더 요구 포맷(예: float planar)으로 변환합니다.
/// 참고: https://ffmpeg.org/doxygen/trunk/group__lswr.html
/// </summary>
private SwrContext* _pSwrContext = null;
/// <summary>
/// 오디오 FIFO 버퍼. 임의 크기의 입력 샘플을 인코더가 요구하는 frame_size 단위로 모아서 전달합니다.
/// 참고: https://ffmpeg.org/doxygen/trunk/group__lavu__audiofifo.html
/// </summary>
private AVAudioFifo* _pAudioFifo = null;
/// <summary>오디오 인코더에 전달할 프레임 구조체.</summary>
private AVFrame* _pAudioFrame = null;
/// <summary>현재까지 인코딩한 오디오 샘플의 누적 인덱스. PTS 값으로 사용됩니다.</summary>
private long _audioSampleIndex;
/// <summary>오디오 인코딩이 활성화되어 있는지 여부.</summary>
private bool _audioEnabled;
/// <summary>생성 시 전달받은 인코딩 설정값 (불변).</summary> /// <summary>생성 시 전달받은 인코딩 설정값 (불변).</summary>
private readonly EncoderSettings _settings; private readonly EncoderSettings _settings;
@@ -236,7 +311,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
byte* buffer = stackalloc byte[(int)bufferSize]; byte* buffer = stackalloc byte[(int)bufferSize];
if (ffmpeg.av_strerror(errnum, buffer, bufferSize) >= 0) if (ffmpeg.av_strerror(errnum, buffer, bufferSize) >= 0)
{ {
return System.Runtime.InteropServices.Marshal.PtrToStringAnsi((IntPtr)buffer) ?? $"알 수 없는 에러 (Code: {errnum})"; return Marshal.PtrToStringAnsi((IntPtr)buffer) ?? $"알 수 없는 에러 (Code: {errnum})";
} }
return $"알 수 없는 에러 (Code: {errnum})"; return $"알 수 없는 에러 (Code: {errnum})";
} }
@@ -282,8 +357,8 @@ namespace ADOFAIRenderer.Util.FFmpeg
// 컨테이너 안에 비디오 트랙(스트림)을 하나 생성합니다. // 컨테이너 안에 비디오 트랙(스트림)을 하나 생성합니다.
// 오디오가 필요하면 여기서 오디오 스트림을 추가로 만들 수 있습니다. // 오디오가 필요하면 여기서 오디오 스트림을 추가로 만들 수 있습니다.
// 참고: https://ffmpeg.org/doxygen/trunk/group__lavf__core.html // 참고: https://ffmpeg.org/doxygen/trunk/group__lavf__core.html
_pStream = ffmpeg.avformat_new_stream(_pFormatContext, null); _pVideoStream = ffmpeg.avformat_new_stream(_pFormatContext, null);
if (_pStream == null) throw new Exception("비디오 스트림을 생성하지 못했습니다."); if (_pVideoStream == null) throw new Exception("비디오 스트림을 생성하지 못했습니다.");
// ── 3단계: 인코더 선택 및 열기 ── // ── 3단계: 인코더 선택 및 열기 ──
// GetEncoderCandidates()가 반환하는 우선순위 목록을 순회하며 // GetEncoderCandidates()가 반환하는 우선순위 목록을 순회하며
@@ -444,7 +519,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
var qsvReady = false; var qsvReady = false;
int err; int err;
bool isWindows = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows); bool isWindows = RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows);
string childDeviceName = isWindows ? "d3d11va" : "vaapi"; string childDeviceName = isWindows ? "d3d11va" : "vaapi";
AVHWDeviceType childDeviceType = isWindows ? AVHWDeviceType.AV_HWDEVICE_TYPE_D3D11VA : AVHWDeviceType.AV_HWDEVICE_TYPE_VAAPI; AVHWDeviceType childDeviceType = isWindows ? AVHWDeviceType.AV_HWDEVICE_TYPE_D3D11VA : AVHWDeviceType.AV_HWDEVICE_TYPE_VAAPI;
@@ -640,9 +715,16 @@ namespace ADOFAIRenderer.Util.FFmpeg
// 코덱 컨텍스트에 설정된 파라미터(해상도, 코덱 ID, 프로파일 등)를 // 코덱 컨텍스트에 설정된 파라미터(해상도, 코덱 ID, 프로파일 등)를
// 스트림의 codecpar에 복사합니다. 컨테이너 헤더에 기록될 정보입니다. // 스트림의 codecpar에 복사합니다. 컨테이너 헤더에 기록될 정보입니다.
// 참고: https://ffmpeg.org/doxygen/trunk/group__lavc__core.html // 참고: https://ffmpeg.org/doxygen/trunk/group__lavc__core.html
if (ffmpeg.avcodec_parameters_from_context(_pStream->codecpar, _pCodecContext) < 0) if (ffmpeg.avcodec_parameters_from_context(_pVideoStream->codecpar, _pCodecContext) < 0)
throw new Exception("코덱 파라미터를 스트림에 복사하는 데 실패했습니다."); throw new Exception("코덱 파라미터를 스트림에 복사하는 데 실패했습니다.");
// ── 4.5단계 (선택): 오디오 스트림 초기화 ──
// EncoderSettings.Audio가 설정되어 있으면 오디오 스트림을 추가합니다.
if (_settings.Audio.HasValue)
{
InitializeAudioStream(_settings.Audio.Value);
}
// ── 5단계: 출력 파일 I/O 열기 ── // ── 5단계: 출력 파일 I/O 열기 ──
// AVFMT_NOFILE 플래그가 없는 포맷은 실제 파일 I/O가 필요합니다. // AVFMT_NOFILE 플래그가 없는 포맷은 실제 파일 I/O가 필요합니다.
// avio_open으로 파일을 쓰기 모드로 열고, 포맷 컨텍스트의 pb(I/O 컨텍스트)에 연결합니다. // avio_open으로 파일을 쓰기 모드로 열고, 포맷 컨텍스트의 pb(I/O 컨텍스트)에 연결합니다.
@@ -1220,8 +1302,8 @@ namespace ADOFAIRenderer.Util.FFmpeg
} }
// 코덱 타임베이스 → 스트림 타임베이스로 PTS/DTS 변환 // 코덱 타임베이스 → 스트림 타임베이스로 PTS/DTS 변환
ffmpeg.av_packet_rescale_ts(_pPacket, _pCodecContext->time_base, _pStream->time_base); ffmpeg.av_packet_rescale_ts(_pPacket, _pCodecContext->time_base, _pVideoStream->time_base);
_pPacket->stream_index = _pStream->index; _pPacket->stream_index = _pVideoStream->index;
// av_interleaved_write_frame: 패킷을 컨테이너에 기록합니다. // av_interleaved_write_frame: 패킷을 컨테이너에 기록합니다.
// "interleaved"는 여러 스트림(비디오+오디오)의 패킷을 시간 순서대로 // "interleaved"는 여러 스트림(비디오+오디오)의 패킷을 시간 순서대로
@@ -1269,7 +1351,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
{ {
if (_disposed || !_isInitialized) return; if (_disposed || !_isInitialized) return;
// 1. 지연된 프레임 플러시 (null 프레임 전송으로 인코더에 "입력 끝" 신호) // 1. 비디오 인코더의 지연된 프레임 플러시 (null 프레임 전송으로 인코더에 "입력 끝" 신호)
try try
{ {
SendFrameAndWritePackets(null); SendFrameAndWritePackets(null);
@@ -1277,7 +1359,20 @@ namespace ADOFAIRenderer.Util.FFmpeg
catch (Exception ex) catch (Exception ex)
{ {
// 플러시 과정 중 발생한 에러 기록 // 플러시 과정 중 발생한 에러 기록
UnityEngine.Debug.LogError($"인코더 플러시 중 오류 발생: {ex.Message}"); UnityEngine.Debug.LogError($"비디오 인코더 플러시 중 오류 발생: {ex.Message}");
}
// 1.5. 오디오 FIFO에 남은 샘플 플러시 + 오디오 인코더 플러시
if (_audioEnabled)
{
try
{
FlushAudioEncoder();
}
catch (Exception ex)
{
UnityEngine.Debug.LogError($"오디오 인코더 플러시 중 오류 발생: {ex.Message}");
}
} }
// 2. 컨테이너 트레일러 작성 (moov atom 등 파일 종료 메타데이터) // 2. 컨테이너 트레일러 작성 (moov atom 등 파일 종료 메타데이터)
@@ -1370,6 +1465,9 @@ namespace ADOFAIRenderer.Util.FFmpeg
_pCodecContext = null; _pCodecContext = null;
} }
// ── 오디오 관련 리소스 해제 ──
DisposeAudioResources();
// 포맷 컨텍스트 및 I/O 닫기 // 포맷 컨텍스트 및 I/O 닫기
if (_pFormatContext != null) if (_pFormatContext != null)
{ {
@@ -1400,5 +1498,371 @@ namespace ADOFAIRenderer.Util.FFmpeg
/// 명시적 Dispose() 호출로 리소스를 관리하세요. /// 명시적 Dispose() 호출로 리소스를 관리하세요.
/// </summary> /// </summary>
~FFmpegNativeEncoder() { Dispose(); } ~FFmpegNativeEncoder() { Dispose(); }
// ================================================================
// 오디오 인코딩 (Audio Encoding)
// ================================================================
/// <summary>
/// 오디오 스트림을 초기화합니다.
///
/// 수행 단계:
/// 1. 오디오 인코더(코덱) 검색 및 코덱 컨텍스트 할당
/// 2. 샘플 레이트, 채널 레이아웃, 샘플 포맷, 비트레이트 설정
/// 3. 오디오 스트림(AVStream) 추가 및 코덱 파라미터 복사
/// 4. 리샘플러(SwrContext) 초기화 (입력 포맷 → 인코더 포맷 변환)
/// 5. FIFO 버퍼 초기화 (임의 크기 입력 → frame_size 단위로 모으기)
/// 6. 오디오 프레임 및 패킷 구조체 할당
///
/// 참고: https://ffmpeg.org/doxygen/trunk/group__lavc__encoding.html
/// </summary>
private void InitializeAudioStream(AudioSettings audioSettings)
{
// ── 1. 오디오 코덱 결정 및 검색 ──
var audioCodecId = audioSettings.AudioCodecId != 0
? audioSettings.AudioCodecId
: AVCodecID.AV_CODEC_ID_AAC;
var pAudioCodec = ffmpeg.avcodec_find_encoder(audioCodecId);
if (pAudioCodec == null)
throw new Exception($"오디오 인코더를 찾을 수 없습니다: {audioCodecId}");
// ── 2. 코덱 컨텍스트 할당 및 파라미터 설정 ──
_pAudioCodecContext = ffmpeg.avcodec_alloc_context3(pAudioCodec);
if (_pAudioCodecContext == null)
throw new Exception("오디오 코덱 컨텍스트를 할당하지 못했습니다.");
var sampleRate = audioSettings.SampleRate > 0 ? audioSettings.SampleRate : 44100;
var channels = audioSettings.Channels > 0 ? audioSettings.Channels : 2;
var bitrate = audioSettings.Bitrate > 0 ? audioSettings.Bitrate : 128000;
// 인코더가 지원하는 샘플 포맷 중 첫 번째를 선택합니다.
// 대부분의 인코더는 AV_SAMPLE_FMT_FLTP(float planar)를 선호합니다.
AVSampleFormat encoderSampleFmt;
#pragma warning disable CS0618 // AVCodec.sample_fmts is deprecated but still functional; avcodec_get_supported_config() is the replacement
if (pAudioCodec->sample_fmts != null)
{
encoderSampleFmt = pAudioCodec->sample_fmts[0];
}
#pragma warning restore CS0618
else
{
encoderSampleFmt = AVSampleFormat.AV_SAMPLE_FMT_FLTP;
}
_pAudioCodecContext->sample_rate = sampleRate;
_pAudioCodecContext->sample_fmt = encoderSampleFmt;
_pAudioCodecContext->bit_rate = bitrate;
_pAudioCodecContext->strict_std_compliance = ffmpeg.FF_COMPLIANCE_EXPERIMENTAL;
// 채널 레이아웃 설정 (FFmpeg 7.x+: AVChannelLayout 사용)
ffmpeg.av_channel_layout_default(&_pAudioCodecContext->ch_layout, channels);
// time_base: 오디오는 일반적으로 1/sample_rate를 사용합니다.
_pAudioCodecContext->time_base = new AVRational { num = 1, den = sampleRate };
// 글로벌 헤더 플래그
if ((_pFormatContext->oformat->flags & ffmpeg.AVFMT_GLOBALHEADER) != 0)
{
_pAudioCodecContext->flags |= ffmpeg.AV_CODEC_FLAG_GLOBAL_HEADER;
}
// 인코더 열기
var ret = ffmpeg.avcodec_open2(_pAudioCodecContext, pAudioCodec, null);
if (ret < 0)
throw new Exception($"오디오 인코더 구동 실패: {GetFFmpegErrorString(ret)} (Code: {ret})");
Melon<RenderMod>.Logger.Msg($"오디오 인코더 구동 성공: {Marshal.PtrToStringAnsi((IntPtr)pAudioCodec->name)} (sample_rate={sampleRate}, channels={channels}, fmt={encoderSampleFmt})");
// ── 3. 오디오 스트림 추가 ──
_pAudioStream = ffmpeg.avformat_new_stream(_pFormatContext, null);
if (_pAudioStream == null)
throw new Exception("오디오 스트림을 생성하지 못했습니다.");
if (ffmpeg.avcodec_parameters_from_context(_pAudioStream->codecpar, _pAudioCodecContext) < 0)
throw new Exception("오디오 코덱 파라미터를 스트림에 복사하는 데 실패했습니다.");
_pAudioStream->time_base = _pAudioCodecContext->time_base;
// ── 4. 리샘플러(SwrContext) 초기화 ──
// 입력: interleaved float (Unity AudioClip.GetData 기본) → 출력: 인코더 요구 포맷
var inputSampleFmt = audioSettings.InputSampleFormat != 0
? audioSettings.InputSampleFormat
: AVSampleFormat.AV_SAMPLE_FMT_FLT;
// SwrContext 할당
fixed (SwrContext** ppSwr = &_pSwrContext)
{
// 입력 채널 레이아웃 설정
AVChannelLayout inLayout;
ffmpeg.av_channel_layout_default(&inLayout, channels);
ret = ffmpeg.swr_alloc_set_opts2(
ppSwr,
&_pAudioCodecContext->ch_layout, encoderSampleFmt, sampleRate,
&inLayout, inputSampleFmt, sampleRate,
0, null);
if (ret < 0 || _pSwrContext == null)
throw new Exception($"오디오 리샘플러 옵션 설정 실패: {GetFFmpegErrorString(ret)} (Code: {ret})");
}
ret = ffmpeg.swr_init(_pSwrContext);
if (ret < 0)
throw new Exception($"오디오 리샘플러 초기화 실패: {GetFFmpegErrorString(ret)} (Code: {ret})");
// ── 5. FIFO 버퍼 초기화 ──
_pAudioFifo = ffmpeg.av_audio_fifo_alloc(encoderSampleFmt, channels, 1);
if (_pAudioFifo == null)
throw new Exception("오디오 FIFO 버퍼를 할당하지 못했습니다.");
// ── 6. 오디오 프레임 및 패킷 할당 ──
_pAudioFrame = ffmpeg.av_frame_alloc();
_pAudioPacket = ffmpeg.av_packet_alloc();
if (_pAudioFrame == null || _pAudioPacket == null)
throw new Exception("오디오 프레임 또는 패킷 구조체를 할당하지 못했습니다.");
// 프레임 크기 결정: 일부 코덱(AAC 등)은 고정 frame_size를 가짐.
// frame_size가 0인 경우(예: PCM 계열) 기본값 1024 사용.
var frameSize = _pAudioCodecContext->frame_size > 0 ? _pAudioCodecContext->frame_size : 1024;
_pAudioFrame->nb_samples = frameSize;
_pAudioFrame->format = (int)encoderSampleFmt;
ffmpeg.av_channel_layout_copy(&_pAudioFrame->ch_layout, &_pAudioCodecContext->ch_layout);
_pAudioFrame->sample_rate = sampleRate;
ret = ffmpeg.av_frame_get_buffer(_pAudioFrame, 0);
if (ret < 0)
throw new Exception($"오디오 프레임 버퍼 할당 실패: {GetFFmpegErrorString(ret)} (Code: {ret})");
_audioSampleIndex = 0;
_audioEnabled = true;
}
/// <summary>
/// 원시 PCM 오디오 샘플 데이터를 받아 오디오 스트림에 인코딩합니다.
///
/// Unity의 AudioClip.GetData()가 반환하는 float[] 배열이나 기타 PCM 소스를 전달할 수 있습니다.
/// 내부적으로 리샘플링 → FIFO 버퍼링 → frame_size 단위 인코딩 파이프라인을 수행합니다.
///
/// 스레드 안전: 내부적으로 _lock을 사용하므로 여러 스레드에서 호출해도 안전합니다.
/// </summary>
/// <param name="samples">PCM 오디오 샘플 데이터. 인터리브드 포맷(LRLRLR...)이어야 합니다.</param>
/// <param name="numSamples">채널당 샘플 수 (= 총 float 개수 / 채널 수).</param>
public void WriteAudioSamples(float[] samples, int numSamples)
{
lock (_lock)
{
if (_disposed || !_isInitialized || !_audioEnabled) return;
fixed (float* pSamples = samples)
{
WriteAudioSamplesInternal((byte*)pSamples, numSamples);
}
}
}
/// <summary>
/// 원시 PCM 오디오 샘플 데이터를 포인터로 받아 오디오 스트림에 인코딩합니다.
/// </summary>
/// <param name="samples">PCM 오디오 샘플 데이터의 포인터.</param>
/// <param name="numSamples">채널당 샘플 수.</param>
public void WriteAudioSamples(IntPtr samples, int numSamples)
{
lock (_lock)
{
if (_disposed || !_isInitialized || !_audioEnabled) return;
WriteAudioSamplesInternal((byte*)samples.ToPointer(), numSamples);
}
}
/// <summary>
/// 오디오 샘플 인코딩의 내부 구현.
///
/// 처리 흐름:
/// 1. swr_convert로 입력 PCM → 인코더 포맷으로 리샘플링
/// 2. 리샘플링된 데이터를 FIFO 버퍼에 추가
/// 3. FIFO에 frame_size 이상 쌓이면 꺼내서 인코더에 전달
///
/// 참고:
/// swr_convert: https://ffmpeg.org/doxygen/trunk/group__lswr.html
/// av_audio_fifo: https://ffmpeg.org/doxygen/trunk/group__lavu__audiofifo.html
/// </summary>
private void WriteAudioSamplesInternal(byte* inputBuffer, int numSamples)
{
// ── 1. 리샘플링: 입력 포맷 → 인코더 포맷 ──
// 출력 버퍼를 위한 프레임 할당 (리샘플링 결과를 담을 임시 프레임)
var pConvertedFrame = ffmpeg.av_frame_alloc();
if (pConvertedFrame == null) return;
try
{
pConvertedFrame->nb_samples = numSamples;
pConvertedFrame->format = (int)_pAudioCodecContext->sample_fmt;
ffmpeg.av_channel_layout_copy(&pConvertedFrame->ch_layout, &_pAudioCodecContext->ch_layout);
pConvertedFrame->sample_rate = _pAudioCodecContext->sample_rate;
var ret = ffmpeg.av_frame_get_buffer(pConvertedFrame, 0);
if (ret < 0)
throw new Exception($"리샘플링 출력 프레임 버퍼 할당 실패: {GetFFmpegErrorString(ret)} (Code: {ret})");
// swr_convert: 입력 데이터를 인코더가 요구하는 포맷으로 변환합니다.
var convertedSamples = ffmpeg.swr_convert(
_pSwrContext,
(byte**)&pConvertedFrame->data, numSamples,
&inputBuffer, numSamples);
if (convertedSamples < 0)
throw new Exception($"오디오 리샘플링 실패: {GetFFmpegErrorString(convertedSamples)} (Code: {convertedSamples})");
if (convertedSamples == 0) return;
// ── 2. FIFO 버퍼에 추가 ──
var written = ffmpeg.av_audio_fifo_write(_pAudioFifo, (void**)&pConvertedFrame->data, convertedSamples);
if (written < convertedSamples)
throw new Exception("오디오 FIFO 쓰기 실패");
// ── 3. FIFO에서 frame_size 단위로 꺼내어 인코딩 ──
var frameSize = _pAudioCodecContext->frame_size > 0 ? _pAudioCodecContext->frame_size : 1024;
while (ffmpeg.av_audio_fifo_size(_pAudioFifo) >= frameSize)
{
ffmpeg.av_frame_make_writable(_pAudioFrame);
_pAudioFrame->nb_samples = frameSize;
var read = ffmpeg.av_audio_fifo_read(_pAudioFifo, (void**)&_pAudioFrame->data, frameSize);
if (read < frameSize)
throw new Exception("오디오 FIFO 읽기 실패");
_pAudioFrame->pts = _audioSampleIndex;
_audioSampleIndex += frameSize;
SendAudioFrameAndWritePackets(_pAudioFrame);
}
}
finally
{
ffmpeg.av_frame_free(&pConvertedFrame);
}
}
/// <summary>
/// 오디오 프레임을 인코더에 전송하고, 결과 패킷을 파일에 기록합니다.
/// 비디오의 SendFrameAndWritePackets와 동일한 Send/Receive 패턴을 사용합니다.
/// </summary>
private void SendAudioFrameAndWritePackets(AVFrame* pFrame)
{
var response = ffmpeg.avcodec_send_frame(_pAudioCodecContext, pFrame);
if (response < 0)
{
throw new Exception($"오디오 인코더에 프레임을 전송하는 도중 에러가 발생했습니다: {GetFFmpegErrorString(response)} (Code: {response})");
}
try
{
while (true)
{
response = ffmpeg.avcodec_receive_packet(_pAudioCodecContext, _pAudioPacket);
if (response == ffmpeg.AVERROR(ffmpeg.EAGAIN) || response == ffmpeg.AVERROR_EOF)
{
break;
}
if (response < 0)
{
throw new Exception($"오디오 인코더에서 패킷을 수신하는 도중 에러가 발생했습니다: {GetFFmpegErrorString(response)} (Code: {response})");
}
// 코덱 타임베이스 → 스트림 타임베이스로 변환
ffmpeg.av_packet_rescale_ts(_pAudioPacket, _pAudioCodecContext->time_base, _pAudioStream->time_base);
_pAudioPacket->stream_index = _pAudioStream->index;
response = ffmpeg.av_interleaved_write_frame(_pFormatContext, _pAudioPacket);
if (response < 0)
{
throw new Exception($"오디오 패킷을 파일에 쓰는 도중 에러가 발생했습니다: {GetFFmpegErrorString(response)} (Code: {response})");
}
ffmpeg.av_packet_unref(_pAudioPacket);
}
}
finally
{
ffmpeg.av_packet_unref(_pAudioPacket);
}
}
/// <summary>
/// 오디오 인코더를 플러시합니다.
/// FIFO에 남아 있는 잔여 샘플(frame_size 미만)을 패딩하여 인코딩한 뒤,
/// null 프레임을 전송하여 인코더의 지연 프레임을 모두 출력합니다.
/// </summary>
private void FlushAudioEncoder()
{
if (_pAudioFifo == null || _pAudioCodecContext == null) return;
// FIFO에 남은 잔여 샘플 인코딩 (frame_size보다 적은 마지막 청크)
var remaining = ffmpeg.av_audio_fifo_size(_pAudioFifo);
if (remaining > 0)
{
ffmpeg.av_frame_make_writable(_pAudioFrame);
_pAudioFrame->nb_samples = remaining;
var read = ffmpeg.av_audio_fifo_read(_pAudioFifo, (void**)&_pAudioFrame->data, remaining);
if (read > 0)
{
_pAudioFrame->pts = _audioSampleIndex;
_audioSampleIndex += read;
SendAudioFrameAndWritePackets(_pAudioFrame);
}
}
// null 프레임 전송으로 인코더 플러시
SendAudioFrameAndWritePackets(null);
}
/// <summary>
/// 오디오 관련 FFmpeg 리소스를 해제합니다.
/// Dispose() 내부에서 호출됩니다.
/// </summary>
private void DisposeAudioResources()
{
if (_pSwrContext != null)
{
fixed (SwrContext** ppSwr = &_pSwrContext)
{
ffmpeg.swr_free(ppSwr);
}
_pSwrContext = null;
}
if (_pAudioFifo != null)
{
ffmpeg.av_audio_fifo_free(_pAudioFifo);
_pAudioFifo = null;
}
if (_pAudioFrame != null)
{
var pFrame = _pAudioFrame;
ffmpeg.av_frame_free(&pFrame);
_pAudioFrame = null;
}
if (_pAudioPacket != null)
{
var pPacket = _pAudioPacket;
ffmpeg.av_packet_free(&pPacket);
_pAudioPacket = null;
}
if (_pAudioCodecContext != null)
{
var pCtx = _pAudioCodecContext;
ffmpeg.avcodec_free_context(&pCtx);
_pAudioCodecContext = null;
}
_audioEnabled = false;
}
} }
} }