はじめに
D言語アドベントカレンダー2020 5日目の記事となります。
アドベントカレンダーへの投稿は、はじめてですが、よろしくお願いします。
予備知識
D言語の記事を初めて読まれる方を想定して、私なりに説明します。
説明不要な方は、本題へお進みください。
D言語とは
D言語はプログラミング言語です。(これは説明するまでもないですね??)
D言語の特徴やいい点を一言で説明するのは難しいので、私のお気に入りのQiita記事を紹介します。
D言語をご紹介したい
ネタじゃないD言語
D言語のUFCSが好きだ!
全ての開発者が学ぶべき1つの言語
D言語のコンパイラについて
D言語コンパイラは、DMD
、GDC
、LDC
の3つほど存在します。
DMD
は公式のD言語コンパイラの位置づけです。
バージョンアップリリースが2か月ごとのペースで続いており、新しい言語仕様を取り込みながら、活発な開発が続いています。
その他にも、gcc
系のGDC
、llvm
系のLDC
が存在しますので、状況に応じて選択可能です。
D言語にはDUBというパッケージマネージャがあります
D言語では、標準ライブラリPhobosの他に、DUBで提供されるライブラリを利用可能です。
DUB
に登録されたライブラリは、統一された方法で取り扱い可能で、ライブラリ間の依存性も解決されます。
活用することで、開発できるアプリケーションの幅が広がります。
SDLとは
SDLは、クロスプラットフォームのマルチメディアライブラリです。グラフィックの描画やサウンドの再生などのAPIを提供しています。インタフェース部はPerl、Python、Ruby、Javaなどのプログラミング言語にも移植されています。(Wikipediaより)
D言語のパッケージとしては、bindbc-sdlとして、利用可能です。
FFMpegとは
FFmpegは、動画と音声を記録・変換・再生するためのフリーソフトウェアです。Unix系オペレーティングシステム (OS) 生まれですが、現在ではクロスプラットフォームです。対応コーデックが多く、多彩なオプションを使用可能なため、幅広く利用されています。 (Wikipediaより)
D言語のパッケージとしては、ffmpeg4dとして、利用可能です。
本題
DUB
パッケージで提供されているbindbc-sdl
、ffmpeg4d
を使って音楽再生を実装します。
64ビット版のアプリケーションを作成する前提です。
環境
- OS : Windows 10 64ビット版 (バージョン 1909)
- D言語コンパイラ : DMD v2.094.0
- 使用するライブラリ
- SDL 2.0.12 (bindbc-sdl バージョン 0.19.2)
- FFMpeg 4.3.1 (ffmpeg4d バージョン 4.1.0)
SDLの入手
これから作成するアプリケーションでは、SDL2.dll
が必要となります。
SDL2.dll
は、ダウンロードページにあるSDL2-2.0.12-win32-x64.zip
から入手します。
FFMpegの入手
コンパイル時にlib
ファイル、アプリケーション実行時にDLL
ファイルが必要となります。
各ファイルは、ダウンロードページで紹介されているサイトから、ビルド済みバイナリを入手するのが簡単です。
この記事では、gyan.devのffmpeg-release-full-shared.7z
(Version 4.3.1-2020-11-08)を使っています。
ソースコード
作成したソースコードを紹介します。
FFmpeg API
の使い方については、こちらのサイトが概要を理解するのに役立ちます。
また、詳細な関数や構造体の使い方は、FFmpeg Documentation(英語サイト)で調べました。
SDL API
については、こちらのサイトが参考になります。
import std.stdio;
import std.utf;
import core.stdc.string;
import bindbc.sdl;
import ffmpeg;
__gshared ubyte[] audiobuf;
__gshared ubyte *audio_pos;
__gshared ulong audio_len;
__gshared int freq;
const CHANNEL = 2;
int main(string[] args)
{
if ( args.length < 2 ){
writeln("Argument required : input file");
return ( 1 );
}
readMediaFile(args[1]);
playMedia();
return ( 0 );
}
void readMediaFile(string fileName)
{
SwrContext* swr = swr_alloc();
scope(exit) swr_free(&swr);
AVFrame* frame = av_frame_alloc();
scope(exit) av_frame_free(&frame);
void initSwr(){
av_opt_set_int(swr, "in_channel_layout", frame.channel_layout, 0);
av_opt_set_int(swr, "out_channel_layout", frame.channel_layout, 0);
av_opt_set_int(swr, "in_sample_rate", frame.sample_rate, 0);
av_opt_set_int(swr, "out_sample_rate", frame.sample_rate, 0);
av_opt_set_sample_fmt(swr, "in_sample_fmt", cast(AVSampleFormat)frame.format, 0);
av_opt_set_sample_fmt(swr, "out_sample_fmt", AVSampleFormat.AV_SAMPLE_FMT_S16, 0);
int ret = swr_init(swr);
if ( ret < 0 ){
writefln("swr_init failed : %d", AVERROR(ret));
writefln("channel_layout : %d", frame.channel_layout);
writefln("sample_rate : %d", frame.sample_rate);
}
freq = frame.sample_rate;
}
void decodeFrame(AVFrame* frame){
int swr_buf_len = frame.nb_samples*frame.channels * 2;
audiobuf.length += swr_buf_len;
ubyte* swr_bufp = audiobuf.ptr;
swr_bufp += audiobuf.length - swr_buf_len;
int ret = swr_convert(swr, &swr_bufp, frame.nb_samples, cast(const ubyte**)frame.extended_data, frame.nb_samples);
if ( ret < 0 ){
writefln("swr_convert failed : %d", AVERROR(ret));
}
}
av_register_all();
AVFormatContext* format_context = null;
const char* input_path = toUTFz!(const(char)*)(fileName);
if ( avformat_open_input(&format_context, input_path, null, null) != 0 ){
writeln("avformat_open_input failed");
writefln("%s", fileName);
}
scope(exit) avformat_close_input(&format_context);
if ( avformat_find_stream_info(format_context, null) < 0 ){
writeln("avformat_find_stream_info failed");
}
AVStream* audio_stream = null;
for ( int i = 0; i < cast(int)format_context.nb_streams; ++i ){
if ( format_context.streams[i].codecpar.codec_type == AVMediaType.AVMEDIA_TYPE_AUDIO ){
audio_stream = format_context.streams[i];
break;
}
}
if ( audio_stream == null ){
writeln("No audio stream ...");
}
AVCodec* codec = avcodec_find_decoder(audio_stream.codecpar.codec_id);
if ( codec == null ){
writeln("No supported decoder ...");
}
AVCodecContext* codec_context = avcodec_alloc_context3(codec);
if ( codec_context == null ){
writeln("avcodec_alloc_context3 failed");
}
scope(exit) avcodec_free_context(&codec_context);
if ( avcodec_parameters_to_context(codec_context, audio_stream.codecpar) < 0 ){
writeln("avcodec_parameters_to_context failed");
}
if ( avcodec_open2(codec_context, codec, null) != 0 ){
writeln("avcodec_open2 failed");
}
AVPacket packet = AVPacket();
while ( av_read_frame(format_context, &packet) == 0 ){
if ( packet.stream_index == audio_stream.index ){
if ( avcodec_send_packet(codec_context, &packet) != 0 ){
writeln("avcodec_send_packet failed");
}
while ( avcodec_receive_frame(codec_context, frame) == 0 ){
if ( swr_is_initialized(swr) == 0 ){
initSwr();
}
decodeFrame(frame);
}
}
av_packet_unref(&packet);
}
// flush decoder
if ( avcodec_send_packet(codec_context, null) != 0 ){
writeln("avcodec_send_packet failed");
}
while ( avcodec_receive_frame(codec_context, frame) == 0 ){
decodeFrame(frame);
}
}
void playMedia()
{
// Load SDL
SDLSupport ret = loadSDL();
if ( ret != sdlSupport ){
if ( ret == SDLSupport.noLibrary ){
writeln("SDLSupport.noLibrary");
} else if ( SDLSupport.badLibrary ){
writeln("SDLSupport.badLibrary");
}
}
scope(exit) unloadSDL();
// Initialize SDL
if ( SDL_Init(SDL_INIT_AUDIO) < 0 ){
writefln("SDL_Init failed : %s", SDL_GetError());
}
scope(exit) SDL_Quit();
SDL_AudioSpec wav_spec;
wav_spec.freq = freq;
wav_spec.format = AUDIO_S16SYS;
wav_spec.channels = CHANNEL;
wav_spec.callback = &audioCallback;
wav_spec.userdata = null;
audio_pos = audiobuf.ptr;
audio_len = audiobuf.length;
if ( SDL_OpenAudio(&wav_spec, null) < 0 ){
writefln("Couldn't open audio: %s", SDL_GetError());
}
scope(exit) SDL_CloseAudio();
SDL_PauseAudio(0);
while ( audio_len > 0 ){
SDL_Delay(200);
}
}
extern(C) nothrow
void audioCallback(void *userdata, ubyte *stream, int len)
{
len = cast(int)( len > audio_len ? audio_len : len );
memcpy(stream, audio_pos, len);
audio_pos += len;
audio_len -= len;
}
{
"name": "playaudio",
"description": "audio file player",
"authors": [
"devmynote"
],
"license": "BSL-1.0",
"dependencies": {
"ffmpeg4d": "~>4.1.0",
"bindbc-sdl": "~>0.19.2",
},
"versions": ["SDL_205","BindSDL_Image"],
"lflags":["/libpath:./lib"],
}
ソースコード補足説明
main
関数では引数をチェックした後、本処理を呼び出します。
readMediaFile
は、パラメータで指定したファイル名から音楽データを抽出し、メモリに格納します。
playMedia
関数は、メモリに格納した音楽データを使って、音楽再生します。
ソースコードで使っているFFmpeg API
とSDL API
の一覧をまとめました。
関数名 | 関数の簡単な説明 |
---|---|
av_register_all | FFmpegのDLLを使用するための初期化 |
avformat_open_input | 指定したファイルを読み、ヘッダ情報をAVFormatContext に格納する |
avformat_close_input |
AVFormatContext を閉じる |
avformat_find_stream_info |
AVFormatContext から、格納されているストリーム情報を取得する |
avcodec_find_decoder | 引数であるコーデックIDに対応したデコーダを取得する |
avcodec_alloc_context3 |
AVCodecContext のメモリを割り当てる |
avcodec_free_context |
AVCodecContext のメモリを開放する |
avcodec_parameters_to_context | 引数であるコーデックパラメータをもとにAVCodecContext に必要な値をセットする |
avcodec_open2 | 引数であるAVCodec をもとにAVCodecContext を初期化する |
av_read_frame | ストリームよりパケットデータを取得する |
avcodec_send_packet | パケットデータをデコーダに渡す |
avcodec_receive_frame | デコーダからフレーム情報を受け取る |
av_packet_unref | パケットデータのメモリを開放する |
av_frame_alloc |
AVFrame のメモリを割り当てる |
av_frame_free |
AVFrame のメモリを開放する |
av_opt_set_int |
SwrContext にパラメータをセットする |
av_opt_set_sample_fmt |
SwrContext に入出力フォーマット情報をセットする |
swr_alloc |
SwrContext のメモリを割り当てる |
swr_free |
SwrContext のメモリを開放する |
swr_is_initialized |
SwrContext が初期化されてるかを調べる |
swr_init |
SwrContext の初期化 |
swr_convert |
SwrContext の情報をもとに入出力フォーマット変換を行う |
関数名 | 関数の簡単な説明 |
---|---|
loadSDL |
bindbc-sdl でSDLを使用する前の初期化 |
unloadSDL |
bindbc-sdl でSDLを使用した後の終了処理 |
SDL_Init | SDLライブラリの初期化 |
SDL_Quit | SDLライブラリの終了処理 |
SDL_OpenAudio | オーディオデバイスを開く |
SDL_CloseAudio | オーディオデバイスを閉じる |
SDL_PauseAudio | オーディオの再生、停止を行う |
SDL_Delay | 指定のミリ秒間待つ |
コンパイル
私の開発環境を例にとると、D:\Dev
が開発用フォルダです。
アプリケーション作成用にplayaudio
フォルダを作成しています。
D:\Dev> cd playaudio
playaudio
内のフォルダ構成はtree
コマンドの出力通りです。
D:\Dev\playaudio> tree
フォルダー パスの一覧: ボリューム ****
ボリューム シリアル番号は ****-**** です
D:.
├─lib
└─source
D:\Dev\playaudio
フォルダにdub.json
とDLL
ファイルを配置します。
DLL
ファイルは、ダウンロードしたSDL2-2.0.12-win32-x64.zip
とffmpeg-release-full-shared.7z
にあります。
D:\Dev\playaudio> dir /B
avcodec-58.dll
avdevice-58.dll
avfilter-7.dll
avformat-58.dll
avutil-56.dll
dub.json
lib
postproc-55.dll
SDL2.dll
source
swresample-3.dll
swscale-5.dll
D:\Dev\playaudio\lib
フォルダにffmpeg-release-full-shared.7z
のlibファイルを配置します。
D:\Dev\playaudio> dir /B lib
avcodec.lib
avdevice.lib
avfilter.lib
avformat.lib
avutil.lib
postproc.lib
swresample.lib
swscale.lib
D:\Dev\playaudio\source
フォルダにapp.d
を配置します。
D:\Dev\playaudio> dir /B source
app.d
各ファイルの配置が完了したら、DUB
コマンドでコンパイルを実行します。
D:\Dev\playaudio> dub build --build=release --arch=x86_64
Fetching bindbc-sdl 0.19.2 (getting selected version)...
Fetching bindbc-loader 0.3.2 (getting selected version)...
Fetching ffmpeg4d 4.1.0 (getting selected version)...
Performing "release" build using D:\App\Dev\dmd2\windows\bin\dmd.exe for x86_64.
bindbc-loader 0.3.2: building configuration "noBC"...
bindbc-sdl 0.19.2: building configuration "dynamic"...
playaudio ~master: building configuration "application"...
Linking...
実行例
コンパイルが無事終了すると、D:\Dev\playaudio
フォルダにplayaudio.exe
が生成されます。
再生したい音楽ファイル名を引数に指定して実行することで、音楽再生ができます。
FFmpeg
が対応しているフォーマットであれば、再生可能です。
※wave
やmp3
ファイルの音楽再生はもちろんのこと、mp4
やmkv
ファイルの音声再生も可能です。
D:\Dev\playaudio> playaudio "再生したい音楽ファイル名"
最後まで読んでいただき、ありがとうございます。
次週に続編の「D言語で動画再生」の記事が掲載される予定です。
興味がありましたら、またお会いしましょう。
おまけ
音楽再生にSDL
を使わず、Windows API
だけで処理したソースコードをおまけに紹介します。
import std.stdio;
import std.utf;
import core.sys.windows.windows;
import core.thread;
import ffmpeg;
pragma(lib, "Winmm.lib");
__gshared ubyte[] audiobuf;
__gshared PCMWAVEFORMAT waveformat;
int main(string[] args)
{
if ( args.length < 2 ){
writeln("Argument required : input file");
return ( 1 );
}
readMediaFile(args[1]);
playMedia();
return ( 0 );
}
void readMediaFile(string fileName)
{
SwrContext* swr = swr_alloc();
scope(exit) swr_free(&swr);
AVFrame* frame = av_frame_alloc();
scope(exit) av_frame_free(&frame);
void initSwr(){
av_opt_set_int(swr, "in_channel_layout", frame.channel_layout, 0);
av_opt_set_int(swr, "out_channel_layout", frame.channel_layout, 0);
av_opt_set_int(swr, "in_sample_rate", frame.sample_rate, 0);
av_opt_set_int(swr, "out_sample_rate", frame.sample_rate, 0);
av_opt_set_sample_fmt(swr, "in_sample_fmt", cast(AVSampleFormat)frame.format, 0);
av_opt_set_sample_fmt(swr, "out_sample_fmt", AVSampleFormat.AV_SAMPLE_FMT_S16, 0);
int ret = swr_init(swr);
if ( ret < 0 ){
writefln("swr_init failed : %d", AVERROR(ret));
writefln("channel_layout : %d", frame.channel_layout);
writefln("sample_rate : %d", frame.sample_rate);
}
waveformat.wf.wFormatTag = WAVE_FORMAT_PCM;
waveformat.wf.nChannels = cast(ushort)frame.channels;
waveformat.wf.nSamplesPerSec = frame.sample_rate;
waveformat.wf.nAvgBytesPerSec = frame.sample_rate * 2;
waveformat.wf.nBlockAlign = cast(ushort)(frame.channels * 2);
waveformat.wBitsPerSample = 16;
}
void decodeFrame(AVFrame* frame){
int swr_buf_len = frame.nb_samples*frame.channels * 2;
audiobuf.length += swr_buf_len;
ubyte* swr_bufp = audiobuf.ptr;
swr_bufp += audiobuf.length - swr_buf_len;
int ret = swr_convert(swr, &swr_bufp, frame.nb_samples, cast(const ubyte**)frame.extended_data, frame.nb_samples);
if ( ret < 0 ){
writefln("swr_convert failed : %d", AVERROR(ret));
}
}
av_register_all();
AVFormatContext* format_context = null;
const char* input_path = toUTFz!(const(char)*)(fileName);
if ( avformat_open_input(&format_context, input_path, null, null) != 0 ){
writeln("avformat_open_input failed");
writefln("%s", fileName);
}
scope(exit) avformat_close_input(&format_context);
if ( avformat_find_stream_info(format_context, null) < 0 ){
writeln("avformat_find_stream_info failed");
}
AVStream* audio_stream = null;
for ( int i = 0; i < cast(int)format_context.nb_streams; ++i ){
if ( format_context.streams[i].codecpar.codec_type == AVMediaType.AVMEDIA_TYPE_AUDIO ){
audio_stream = format_context.streams[i];
break;
}
}
if ( audio_stream == null ){
writeln("No audio stream ...");
}
AVCodec* codec = avcodec_find_decoder(audio_stream.codecpar.codec_id);
if ( codec == null ){
writeln("No supported decoder ...");
}
AVCodecContext* codec_context = avcodec_alloc_context3(codec);
if ( codec_context == null ){
writeln("avcodec_alloc_context3 failed");
}
scope(exit) avcodec_free_context(&codec_context);
if ( avcodec_parameters_to_context(codec_context, audio_stream.codecpar) < 0 ){
writeln("avcodec_parameters_to_context failed");
}
if ( avcodec_open2(codec_context, codec, null) != 0 ){
writeln("avcodec_open2 failed");
}
AVPacket packet = AVPacket();
while ( av_read_frame(format_context, &packet) == 0 ){
if ( packet.stream_index == audio_stream.index ){
if ( avcodec_send_packet(codec_context, &packet) != 0 ){
writeln("avcodec_send_packet failed");
}
while ( avcodec_receive_frame(codec_context, frame) == 0 ){
if ( swr_is_initialized(swr) == 0 ){
initSwr();
}
decodeFrame(frame);
}
}
av_packet_unref(&packet);
}
// flush decoder
if ( avcodec_send_packet(codec_context, null) != 0 ){
writeln("avcodec_send_packet failed");
}
while ( avcodec_receive_frame(codec_context, frame) == 0 ){
decodeFrame(frame);
}
}
void playMedia()
{
HWAVEOUT hwaveout;
int ret;
ret = waveOutOpen(&hwaveout, WAVE_MAPPER, cast(WAVEFORMATEX*)&waveformat, 0, 0, CALLBACK_NULL);
if ( ret != 0 ){
writefln("waveOutOpen failed : %d", ret);
return;
}
scope(exit) waveOutClose(hwaveout);
WAVEHDR waveheader;
waveheader.lpData = cast(char*)audiobuf.ptr;
waveheader.dwBufferLength = cast(uint)audiobuf.length;
// waveheader.dwBytesRecorded;
// waveheader.dwUser;
waveheader.dwFlags = 0;
waveheader.dwLoops = 0;
// waveheader.lpNext;
// waveheader.reserved;
ret = waveOutPrepareHeader(hwaveout, &waveheader, WAVEHDR.sizeof);
if ( ret != 0 ){
writefln("waveOutPrepareHeader failed : %d", ret);
return;
}
scope(exit) waveOutUnprepareHeader(hwaveout, &waveheader, WAVEHDR.sizeof);
ret = waveOutWrite(hwaveout, &waveheader, WAVEHDR.sizeof);
if ( ret != 0 ){
writefln("waveOutWrite failed : %d", ret);
return;
}
MMTIME mm;
mm.wType = TIME_BYTES;
do {
waveOutGetPosition(hwaveout, &mm, mm.sizeof);
// writefln("%d / %d", mm.cb, audiobuf.length);
Thread.sleep(dur!("msecs")(200));
} while ( mm.cb < audiobuf.length );
}