C# - ffmpeg(FFmpeg.AutoGen)를 이용해 MP2 오디오 파일 디코딩 예제(decode_audio.c)
지난 글에 이어,
C# - ffmpeg(FFmpeg.AutoGen)를 이용한 비디오 디코딩 예제(decode_video.c)
; https://www.sysnet.pe.kr/2/0/12924
이번에는
ffmpeg 예제 중 "
decode_audio.c" 파일을 포팅하겠습니다.
예제를 가만 보니, 코덱으로 AV_CODEC_ID_MP2를 쓰고 있는데요, 따라서 이 예제가 동작하려면 MP2 파일이 필요합니다. 없다면 아래와 같은 명령어로 기존 mp3 파일로부터 구할 수 있습니다.
c:\temp> ffmpeg -i "test.mp3" -acodec mp2 "test.mp2"
변환된 파일을 살펴보면,
c:\temp> ffprobe "test.mp2"
ffprobe version 4.4.1 Copyright (c) 2007-2021 the FFmpeg developers
built with Microsoft (R) C/C++ Optimizing Compiler Version 19.30.30705 for x64
configuration: ...[생략]... --extra-cflags=-MD --extra-cxxflags=-MD
libavutil 56. 70.100 / 56. 70.100
libavcodec 58.134.100 / 58.134.100
libavformat 58. 76.100 / 58. 76.100
libavdevice 58. 13.100 / 58. 13.100
libavfilter 7.110.100 / 7.110.100
libswscale 5. 9.100 / 5. 9.100
libswresample 3. 9.100 / 3. 9.100
libpostproc 55. 9.100 / 55. 9.100
[mp3 @ 0000014BC8BF0740] Estimating duration from bitrate, this may be inaccurate
Input #0, mp3, from 'test.mp2':
Duration: 00:04:00.35, start: 0.000000, bitrate: 383 kb/s
Stream #0:0: Audio: mp2, 44100 Hz, stereo, fltp, 384 kb/s
"Input #0, mp3"라고는 하지만 내부 Stream은 mp2라고 나옵니다.
그래서 결국 이렇게 포팅을 할 수 있습니다.
using FFmpeg.AutoGen;
using FFmpeg.AutoGen.Example;
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace FFmpegApp1
{
internal unsafe class Program
{
[DllImport("Kernel32.dll", EntryPoint = "RtlMoveMemory", SetLastError = false)]
static extern void MoveMemory(IntPtr dest, IntPtr src, int size);
const int AUDIO_INBUF_SIZE = 20480;
const int AUDIO_REFILL_THRESH = 4096;
static void Main(string[] args)
{
FFmpegBinariesHelper.RegisterFFmpegBinaries();
#if DEBUG
Console.WriteLine("Current directory: " + Environment.CurrentDirectory);
Console.WriteLine("Running in {0}-bit mode.", Environment.Is64BitProcess ? "64" : "32");
Console.WriteLine($"FFmpeg version info: {ffmpeg.av_version_info()}");
#endif
Console.WriteLine();
Console.WriteLine($"LIBAVFORMAT Version: {ffmpeg.LIBAVFORMAT_VERSION_MAJOR}.{ffmpeg.LIBAVFORMAT_VERSION_MINOR}");
string outputPath = @"c:\temp\output";
try
{
Directory.Delete(outputPath, true);
Directory.CreateDirectory(outputPath);
}
catch { }
string outputFilePath = Path.Combine(outputPath, "test_cs2.dat");
decode_audio(@"D:\media_sample\test.mp3", outputFilePath);
}
static unsafe void decode_audio(string inputFileName, string outputFileName)
{
AVCodec* codec = null;
AVPacket* packet = null;
AVCodecParserContext* parser = null;
AVCodecContext* c = null;
AVFrame* decoded_frame = null;
int ret;
do
{
packet = ffmpeg.av_packet_alloc();
codec = ffmpeg.avcodec_find_decoder(AVCodecID.AV_CODEC_ID_MP2);
if (codec == null)
{
Console.WriteLine("Codec not found");
break;
}
parser = ffmpeg.av_parser_init((int)codec->id);
if (parser == null)
{
Console.WriteLine("Parser not found");
break;
}
c = ffmpeg.avcodec_alloc_context3(codec);
if (c == null)
{
Console.WriteLine("Could not allocate audio codec context");
break;
}
if (ffmpeg.avcodec_open2(c, codec, null) < 0)
{
Console.WriteLine("Could not open codec");
break;
}
using FileStream f = File.OpenRead(inputFileName);
using FileStream outfile = File.OpenWrite(outputFileName);
byte[] inbuf = new byte[AUDIO_INBUF_SIZE + ffmpeg.AV_INPUT_BUFFER_PADDING_SIZE];
fixed (byte* ptr = &inbuf[0])
{
byte* data = ptr;
int data_size = f.Read(inbuf, 0, AUDIO_INBUF_SIZE);
while (data_size > 0)
{
if (decoded_frame == null)
{
decoded_frame = ffmpeg.av_frame_alloc();
if (decoded_frame == null)
{
Console.WriteLine("Could not allocate audio frame");
break;
}
}
ret = ffmpeg.av_parser_parse2(parser, c, &packet->data, &packet->size,
data, data_size, ffmpeg.AV_NOPTS_VALUE, ffmpeg.AV_NOPTS_VALUE, 0);
if (ret < 0)
{
Console.WriteLine("Error while parsing");
break;
}
data += ret;
data_size -= ret;
if (packet->size != 0)
{
if (decode(c, packet, decoded_frame, outfile) == false)
{
break;
}
}
if (data_size < AUDIO_REFILL_THRESH)
{
fixed (byte* temp_ptr = &inbuf[0])
{
MoveMemory(new IntPtr(temp_ptr), new IntPtr(data), data_size);
data = temp_ptr;
int len = f.Read(inbuf, data_size, AUDIO_INBUF_SIZE - data_size);
if (len > 0)
{
data_size += len;
}
}
}
}
}
packet->data = null;
packet->size = 0;
if (decode(c, packet, decoded_frame, outfile) == false)
{
break;
}
AVSampleFormat sfmt = c->sample_fmt;
if (ffmpeg.av_sample_fmt_is_planar(sfmt) != 0)
{
string packed = ffmpeg.av_get_sample_fmt_name(sfmt);
Console.WriteLine($"Warning: the sample format the decoder produced is planar {0}. This example will output the first channel only.",
packed == null ? "?" : packed);
sfmt = ffmpeg.av_get_packed_sample_fmt(sfmt);
}
int n_channels = c->channels;
string fmt = get_format_from_sample_fmt(sfmt);
if (fmt == null)
{
break;
}
Console.WriteLine("Play the output audio file with the command:\n" +
$"ffplay -f {fmt} -ac {n_channels} -ar {c->sample_rate} {outputFileName}\n");
} while (false);
if (c != null)
{
ffmpeg.avcodec_free_context(&c);
}
if (parser != null)
{
ffmpeg.av_parser_close(parser);
}
if (decoded_frame != null)
{
ffmpeg.av_frame_free(&decoded_frame);
}
if (packet != null)
{
ffmpeg.av_packet_free(&packet);
}
}
private unsafe static bool decode(AVCodecContext* dec_ctx, AVPacket* packet, AVFrame* frame, FileStream outfile)
{
int i;
int ch;
int ret;
int data_size;
ret = ffmpeg.avcodec_send_packet(dec_ctx, packet);
if (ret < 0)
{
Console.WriteLine("Error submitting the packet to the decoder");
return false;
}
while (ret >= 0)
{
ret = ffmpeg.avcodec_receive_frame(dec_ctx, frame);
if (ret == ffmpeg.AVERROR(ffmpeg.EAGAIN) || ret == ffmpeg.AVERROR_EOF)
{
return true;
} else if (ret < 0)
{
Console.WriteLine("Error during decoding");
return false;
}
data_size = ffmpeg.av_get_bytes_per_sample(dec_ctx->sample_fmt);
if (data_size < 0)
{
Console.WriteLine("Failed to calculate data size");
return false;
}
for (i = 0; i < frame->nb_samples; i ++)
{
for (ch = 0; ch < dec_ctx->channels; ch ++)
{
byte *ptr = frame->data[0];
ptr += (data_size * i);
ReadOnlySpan<byte> data = new ReadOnlySpan<byte>(ptr, data_size);
outfile.Write(data);
}
}
}
return true;
}
static unsafe string get_format_from_sample_fmt(AVSampleFormat sample_fmt)
{
sample_fmt_entry[] sample_fmt_entries = {
new sample_fmt_entry(AVSampleFormat.AV_SAMPLE_FMT_U8, "u8", "u8"),
new sample_fmt_entry(AVSampleFormat.AV_SAMPLE_FMT_S16, "s16be", "s16le"),
new sample_fmt_entry(AVSampleFormat.AV_SAMPLE_FMT_S32, "s32be", "s32le"),
new sample_fmt_entry(AVSampleFormat.AV_SAMPLE_FMT_FLT, "f32be", "f32le"),
new sample_fmt_entry(AVSampleFormat.AV_SAMPLE_FMT_DBL, "f64be", "f64le"),
};
foreach (var entry in sample_fmt_entries)
{
if (entry.SampleFormat == sample_fmt)
{
if (BitConverter.IsLittleEndian == true)
{
return entry.FormatLE;
}
return entry.FoamtBE;
}
}
Console.WriteLine($"sample format {ffmpeg.av_get_sample_fmt_name(sample_fmt)} is not supported as output format");
return null;
}
}
struct sample_fmt_entry
{
AVSampleFormat sample_fmt;
public AVSampleFormat SampleFormat => sample_fmt;
string fmt_be;
public string FoamtBE => fmt_be;
string fmt_le;
public string FormatLE => fmt_le;
public sample_fmt_entry(AVSampleFormat fmt, string be, string le)
{
sample_fmt = fmt;
fmt_be = be;
fmt_le = le;
}
}
}
(
첨부 파일은 이 글의 예제 코드를 포함합니다.)
(
이 글의 소스 코드는 github에 올려져 있습니다.)
해당 파일들의 크기를 비교하면 이렇습니다.
원본 mp3: 5,752KB
mp2: 11,267KB
decode_audio.c: 41,405KB
그리고 출력된 파일은 나름 포맷을 가지고 있습니다.
D:\media_sample> ffprobe C:\temp\output\test_cs2.dat
ffprobe version 4.4.1 Copyright (c) 2007-2021 the FFmpeg developers
built with Microsoft (R) C/C++ Optimizing Compiler Version 19.30.30705 for x64
configuration: ...[생략]...
libpostproc 55. 9.100 / 55. 9.100
[adp @ 000002C510D30000] Format adp detected only with low score of 25, misdetection possible!
Input #0, adp, from 'C:\temp\output\test_cs2.dat':
Duration: 00:12:52.88, start: 0.000000, bitrate: 438 kb/s
Stream #0:0: Audio: adpcm_dtk, 48000 Hz, stereo, s16p
재미있는 것은, 예제(decode_audio)를 실행하면 어떻게 음악을 재생할 수 있는지도 출력을 해줍니다. ^^
Warning: the sample format the decoder produced is planar 0. This example will output the first channel only.
Play the output audio file with the command:
ffplay -f s16le -ac 2 -ar 44100 c:\temp\output\test_cs2.dat
실제로 저 명령으로 실행하면 다음과 같은 식의 창이 뜨고,
음악이 재생됩니다.
혹시나 싶어 MP3 파일로 해당 예제를 실행했더니 다음과 같은 오류 메시지가 발생합니다.
[mp2 @ 0000015BA400C000] Header missing
Error submitting the packet to the decoder
그렇다면, 저 소스 코드 그대로 단지 codec ID 값만 MP3로 바꾸면 어떨까요?
codec = ffmpeg.avcodec_find_decoder(AVCodecID.AV_CODEC_ID_MP3);
아쉽게도, 여전히 "Header missing" 오류가 발생합니다. 뭐랄까... 추상화를 잘 했다면 분명히 코덱 ID만 바꿔 전달하는 것으로 동작을 했어야 하지 않을까요? ^^
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]