LoginSignup
9
2

More than 3 years have passed since last update.

D言語で音楽再生

Last updated at Posted at 2020-12-04

はじめに

D言語アドベントカレンダー2020 5日目の記事となります。
アドベントカレンダーへの投稿は、はじめてですが、よろしくお願いします。

予備知識

D言語の記事を初めて読まれる方を想定して、私なりに説明します。
説明不要な方は、本題へお進みください。

D言語とは

D言語はプログラミング言語です。(これは説明するまでもないですね??)
D言語の特徴やいい点を一言で説明するのは難しいので、私のお気に入りのQiita記事を紹介します。
D言語をご紹介したい
ネタじゃないD言語
D言語のUFCSが好きだ!
全ての開発者が学ぶべき1つの言語

D言語のコンパイラについて

D言語コンパイラは、DMDGDCLDCの3つほど存在します。
DMDは公式のD言語コンパイラの位置づけです。
バージョンアップリリースが2か月ごとのペースで続いており、新しい言語仕様を取り込みながら、活発な開発が続いています。
その他にも、gcc系のGDCllvm系の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-sdlffmpeg4dを使って音楽再生を実装します。
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.devffmpeg-release-full-shared.7z(Version 4.3.1-2020-11-08)を使っています。

ソースコード

作成したソースコードを紹介します。
FFmpeg APIの使い方については、こちらのサイトが概要を理解するのに役立ちます。
また、詳細な関数や構造体の使い方は、FFmpeg Documentation(英語サイト)で調べました。
SDL APIについては、こちらのサイトが参考になります。

app.d
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;
}
dub.json
{
    "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 APISDL 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.jsonDLLファイルを配置します。
DLLファイルは、ダウンロードしたSDL2-2.0.12-win32-x64.zipffmpeg-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が対応しているフォーマットであれば、再生可能です。
wavemp3ファイルの音楽再生はもちろんのこと、mp4mkvファイルの音声再生も可能です。

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 );
}
9
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
2