diff --git a/Util/FFmpeg/FFmpegNativeEncoder.cs b/Util/FFmpeg/FFmpegNativeEncoder.cs
index 8a1cc26..27162f4 100644
--- a/Util/FFmpeg/FFmpegNativeEncoder.cs
+++ b/Util/FFmpeg/FFmpegNativeEncoder.cs
@@ -1,4 +1,5 @@
using System;
+using System.Runtime.InteropServices;
using FFmpeg.AutoGen;
using MelonLoader;
using Unity.Collections;
@@ -40,6 +41,36 @@ namespace ADOFAIRenderer.Util.FFmpeg
///
/// 인코더에 전달할 설정값을 담는 구조체입니다.
///
+ ///
+ /// 오디오 인코딩에 전달할 설정값을 담는 구조체입니다.
+ ///
+ public struct AudioSettings
+ {
+ /// 입력 오디오의 샘플 레이트 (Hz). 예: 44100, 48000.
+ public int SampleRate;
+
+ /// 입력 오디오의 채널 수. 예: 1(모노), 2(스테레오).
+ public int Channels;
+
+ ///
+ /// 입력 오디오의 샘플 포맷. 기본값은 AV_SAMPLE_FMT_FLT (32-bit float).
+ /// Unity의 AudioClip.GetData()는 float[] 형태이므로 FLT가 적합합니다.
+ ///
+ public AVSampleFormat InputSampleFormat;
+
+ ///
+ /// 사용할 오디오 코덱 ID. 기본값(0)이면 AAC를 사용합니다.
+ /// 예: AV_CODEC_ID_AAC, AV_CODEC_ID_FLAC, AV_CODEC_ID_OPUS 등.
+ ///
+ public AVCodecID AudioCodecId;
+
+ ///
+ /// 오디오 비트레이트 (bps 단위). 기본값(0)이면 128000 bps를 사용합니다.
+ /// AAC의 경우 128000~320000 bps가 일반적입니다.
+ ///
+ public int Bitrate;
+ }
+
public struct EncoderSettings
{
/// 출력 비디오의 가로 해상도 (픽셀).
@@ -71,6 +102,12 @@ namespace ADOFAIRenderer.Util.FFmpeg
/// 소프트웨어 CPU 인코더를 강제 사용할지 여부.
public bool ForceSoftware;
+
+ ///
+ /// 오디오 인코딩 설정. null이면 오디오 스트림 없이 비디오만 인코딩합니다 (기존 동작과 동일).
+ /// 값을 설정하면 비디오와 오디오가 함께 먹싱(muxing)됩니다.
+ ///
+ public AudioSettings? Audio;
}
// ──────────────────────────────────────────────────────────────
@@ -97,7 +134,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
/// 여기서는 비디오 스트림 하나만 사용합니다.
/// 참고: https://ffmpeg.org/doxygen/trunk/group__lavf__core.html
///
- private AVStream* _pStream = null;
+ private AVStream* _pVideoStream = null;
// ──────────────────────────────────────────────────────────────
// 프레임 버퍼 및 색공간 변환기(스케일러) 포인터들
@@ -174,6 +211,44 @@ namespace ADOFAIRenderer.Util.FFmpeg
private AVFrame* _pHwFrame = null;
private bool _loggedConversionMode;
+ // ──────────────────────────────────────────────────────────────
+ // 오디오 인코딩 관련 FFmpeg 구조체 포인터들
+ // ──────────────────────────────────────────────────────────────
+
+ ///
+ /// 오디오 코덱(인코더)의 상태를 관리하는 컨텍스트.
+ /// 샘플 레이트, 채널 레이아웃, 샘플 포맷 등 오디오 인코딩 파라미터가 설정됩니다.
+ ///
+ private AVCodecContext* _pAudioCodecContext = null;
+
+ /// 출력 파일 내의 오디오 스트림.
+ private AVStream* _pAudioStream = null;
+
+ /// 오디오 인코딩된 패킷 데이터를 임시로 담을 패킷 구조체.
+ private AVPacket* _pAudioPacket = null;
+
+ ///
+ /// 오디오 리샘플링 컨텍스트 (SwrContext).
+ /// 입력 PCM 포맷(예: float interleaved) → 인코더 요구 포맷(예: float planar)으로 변환합니다.
+ /// 참고: https://ffmpeg.org/doxygen/trunk/group__lswr.html
+ ///
+ private SwrContext* _pSwrContext = null;
+
+ ///
+ /// 오디오 FIFO 버퍼. 임의 크기의 입력 샘플을 인코더가 요구하는 frame_size 단위로 모아서 전달합니다.
+ /// 참고: https://ffmpeg.org/doxygen/trunk/group__lavu__audiofifo.html
+ ///
+ private AVAudioFifo* _pAudioFifo = null;
+
+ /// 오디오 인코더에 전달할 프레임 구조체.
+ private AVFrame* _pAudioFrame = null;
+
+ /// 현재까지 인코딩한 오디오 샘플의 누적 인덱스. PTS 값으로 사용됩니다.
+ private long _audioSampleIndex;
+
+ /// 오디오 인코딩이 활성화되어 있는지 여부.
+ private bool _audioEnabled;
+
/// 생성 시 전달받은 인코딩 설정값 (불변).
private readonly EncoderSettings _settings;
@@ -236,7 +311,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
byte* buffer = stackalloc byte[(int)bufferSize];
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})";
}
@@ -282,8 +357,8 @@ namespace ADOFAIRenderer.Util.FFmpeg
// 컨테이너 안에 비디오 트랙(스트림)을 하나 생성합니다.
// 오디오가 필요하면 여기서 오디오 스트림을 추가로 만들 수 있습니다.
// 참고: https://ffmpeg.org/doxygen/trunk/group__lavf__core.html
- _pStream = ffmpeg.avformat_new_stream(_pFormatContext, null);
- if (_pStream == null) throw new Exception("비디오 스트림을 생성하지 못했습니다.");
+ _pVideoStream = ffmpeg.avformat_new_stream(_pFormatContext, null);
+ if (_pVideoStream == null) throw new Exception("비디오 스트림을 생성하지 못했습니다.");
// ── 3단계: 인코더 선택 및 열기 ──
// GetEncoderCandidates()가 반환하는 우선순위 목록을 순회하며
@@ -444,7 +519,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
var qsvReady = false;
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";
AVHWDeviceType childDeviceType = isWindows ? AVHWDeviceType.AV_HWDEVICE_TYPE_D3D11VA : AVHWDeviceType.AV_HWDEVICE_TYPE_VAAPI;
@@ -640,9 +715,16 @@ namespace ADOFAIRenderer.Util.FFmpeg
// 코덱 컨텍스트에 설정된 파라미터(해상도, 코덱 ID, 프로파일 등)를
// 스트림의 codecpar에 복사합니다. 컨테이너 헤더에 기록될 정보입니다.
// 참고: 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("코덱 파라미터를 스트림에 복사하는 데 실패했습니다.");
+ // ── 4.5단계 (선택): 오디오 스트림 초기화 ──
+ // EncoderSettings.Audio가 설정되어 있으면 오디오 스트림을 추가합니다.
+ if (_settings.Audio.HasValue)
+ {
+ InitializeAudioStream(_settings.Audio.Value);
+ }
+
// ── 5단계: 출력 파일 I/O 열기 ──
// AVFMT_NOFILE 플래그가 없는 포맷은 실제 파일 I/O가 필요합니다.
// avio_open으로 파일을 쓰기 모드로 열고, 포맷 컨텍스트의 pb(I/O 컨텍스트)에 연결합니다.
@@ -1220,8 +1302,8 @@ namespace ADOFAIRenderer.Util.FFmpeg
}
// 코덱 타임베이스 → 스트림 타임베이스로 PTS/DTS 변환
- ffmpeg.av_packet_rescale_ts(_pPacket, _pCodecContext->time_base, _pStream->time_base);
- _pPacket->stream_index = _pStream->index;
+ ffmpeg.av_packet_rescale_ts(_pPacket, _pCodecContext->time_base, _pVideoStream->time_base);
+ _pPacket->stream_index = _pVideoStream->index;
// av_interleaved_write_frame: 패킷을 컨테이너에 기록합니다.
// "interleaved"는 여러 스트림(비디오+오디오)의 패킷을 시간 순서대로
@@ -1269,7 +1351,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
{
if (_disposed || !_isInitialized) return;
- // 1. 지연된 프레임 플러시 (null 프레임 전송으로 인코더에 "입력 끝" 신호)
+ // 1. 비디오 인코더의 지연된 프레임 플러시 (null 프레임 전송으로 인코더에 "입력 끝" 신호)
try
{
SendFrameAndWritePackets(null);
@@ -1277,7 +1359,20 @@ namespace ADOFAIRenderer.Util.FFmpeg
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 등 파일 종료 메타데이터)
@@ -1370,6 +1465,9 @@ namespace ADOFAIRenderer.Util.FFmpeg
_pCodecContext = null;
}
+ // ── 오디오 관련 리소스 해제 ──
+ DisposeAudioResources();
+
// 포맷 컨텍스트 및 I/O 닫기
if (_pFormatContext != null)
{
@@ -1400,5 +1498,371 @@ namespace ADOFAIRenderer.Util.FFmpeg
/// 명시적 Dispose() 호출로 리소스를 관리하세요.
///
~FFmpegNativeEncoder() { Dispose(); }
+
+ // ================================================================
+ // 오디오 인코딩 (Audio Encoding)
+ // ================================================================
+
+ ///
+ /// 오디오 스트림을 초기화합니다.
+ ///
+ /// 수행 단계:
+ /// 1. 오디오 인코더(코덱) 검색 및 코덱 컨텍스트 할당
+ /// 2. 샘플 레이트, 채널 레이아웃, 샘플 포맷, 비트레이트 설정
+ /// 3. 오디오 스트림(AVStream) 추가 및 코덱 파라미터 복사
+ /// 4. 리샘플러(SwrContext) 초기화 (입력 포맷 → 인코더 포맷 변환)
+ /// 5. FIFO 버퍼 초기화 (임의 크기 입력 → frame_size 단위로 모으기)
+ /// 6. 오디오 프레임 및 패킷 구조체 할당
+ ///
+ /// 참고: https://ffmpeg.org/doxygen/trunk/group__lavc__encoding.html
+ ///
+ 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.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;
+ }
+
+ ///
+ /// 원시 PCM 오디오 샘플 데이터를 받아 오디오 스트림에 인코딩합니다.
+ ///
+ /// Unity의 AudioClip.GetData()가 반환하는 float[] 배열이나 기타 PCM 소스를 전달할 수 있습니다.
+ /// 내부적으로 리샘플링 → FIFO 버퍼링 → frame_size 단위 인코딩 파이프라인을 수행합니다.
+ ///
+ /// 스레드 안전: 내부적으로 _lock을 사용하므로 여러 스레드에서 호출해도 안전합니다.
+ ///
+ /// PCM 오디오 샘플 데이터. 인터리브드 포맷(LRLRLR...)이어야 합니다.
+ /// 채널당 샘플 수 (= 총 float 개수 / 채널 수).
+ public void WriteAudioSamples(float[] samples, int numSamples)
+ {
+ lock (_lock)
+ {
+ if (_disposed || !_isInitialized || !_audioEnabled) return;
+
+ fixed (float* pSamples = samples)
+ {
+ WriteAudioSamplesInternal((byte*)pSamples, numSamples);
+ }
+ }
+ }
+
+ ///
+ /// 원시 PCM 오디오 샘플 데이터를 포인터로 받아 오디오 스트림에 인코딩합니다.
+ ///
+ /// PCM 오디오 샘플 데이터의 포인터.
+ /// 채널당 샘플 수.
+ public void WriteAudioSamples(IntPtr samples, int numSamples)
+ {
+ lock (_lock)
+ {
+ if (_disposed || !_isInitialized || !_audioEnabled) return;
+ WriteAudioSamplesInternal((byte*)samples.ToPointer(), numSamples);
+ }
+ }
+
+ ///
+ /// 오디오 샘플 인코딩의 내부 구현.
+ ///
+ /// 처리 흐름:
+ /// 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
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// 오디오 프레임을 인코더에 전송하고, 결과 패킷을 파일에 기록합니다.
+ /// 비디오의 SendFrameAndWritePackets와 동일한 Send/Receive 패턴을 사용합니다.
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// 오디오 인코더를 플러시합니다.
+ /// FIFO에 남아 있는 잔여 샘플(frame_size 미만)을 패딩하여 인코딩한 뒤,
+ /// null 프레임을 전송하여 인코더의 지연 프레임을 모두 출력합니다.
+ ///
+ 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);
+ }
+
+ ///
+ /// 오디오 관련 FFmpeg 리소스를 해제합니다.
+ /// Dispose() 내부에서 호출됩니다.
+ ///
+ 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;
+ }
}
}
\ No newline at end of file