アカベコマイリ

HEAR NOTHING SEE NOTHING SAY NOTHING

LAME で MP3 エンコード

プログラム上で WAV を MP3 に変換したくなり方法を検討してみたところ LAME を使用するのがよさそうだった。簡単なサンプル プログラムを作りながら使いかたを学んでみる。

LAME は様々なプラットフォーム向けにインターフェースを提供しているが、この記事では Windows 向けの DLL を Visual Studio 2010 の MFC ダイアログ アプリから利用する。

LAME の入手とビルド

はじめに公式ページから LAME を入手。

現時点の最新バージョンは 3.98.4。上記ページのリンク先に lame-3.98.4.tar.gz というファイルが公開されているのでダウンロード後に展開する。展開されたフォルダ内には lamevc8.sln というファイルがあるのでこれを Visual Studio 2010 で開き変換ウィザードで 2010 用にする。**mp3xvc8 というプロジェクトでエラーになるけれど、これは不要**なので気にしなくてもよい。

Visual Studio でソリューションを開いたら LameDll_vc8 というプロジェクト内にある BladeMP3EncDLL.h というヘッダ ファイルを以下のように編集。

// added for floating point audio  -- DSPguru, jd
typedef BE_ERR  (*BEENCODECHUNKFLOATS16NI)  (HBE_STREAM, DWORD, PFLOAT, PFLOAT, PBYTE, PDWORD);
typedef BE_ERR  (*BEDEINITSTREAM)           (HBE_STREAM, PBYTE, PDWORD);
typedef BE_ERR  (*BECLOSESTREAM)            (HBE_STREAM);
typedef VOID    (*BEVERSION)                (PBE_VERSION);
typedef BE_ERR  (*BEWRITEVBRHEADER)         (LPCWSTR);
typedef BE_ERR  (*BEWRITEINFOTAG)           (HBE_STREAM, LPCWSTR );

プロジェクト設定がユニコード ビルドとなっているうえ beWriteInfoTag と beWriteVBRHeader はファイルを操作する関数なので、関数のパスにあたる引数の型を LPCSTR から LPCWSTR に変更している。

次に実装側も直す。BladeMP3EncDLL.c を開き、beWriteInfoTag を修正。パスの型を LPCWSTR に変更し、fopen を wfopens にしておく。他にも直したい箇所は数多くあるのだが、とりあえずは限定的な変更にとどめておく。

__declspec(dllexport) BE_ERR beWriteInfoTag( HBE_STREAM hbeStream,
                                             LPCWSTR lpszFileName )
{
    FILE* fpStream  = NULL;
    BE_ERR beResult = BE_ERR_SUCCESSFUL;

    lame_global_flags*  gfp = (lame_global_flags*)hbeStream;

    if ( NULL != gfp )
    {
        // Do we have to write the VBR tag?
        if ( lame_get_bWriteVbrTag( gfp ) )
        {
            // Try to open the file, And check file open result
            if ( _wfopen_s( &fpStream, lpszFileName, L"rb+" ) != 0 )

beWriteVBRHeader を修正。これは単なる beWriteInfoTag の Wrapper なので引数の型だけ変更すればよい。

// for backwards compatiblity
__declspec(dllexport) BE_ERR beWriteVBRHeader(LPCWSTR lpszFileName)
{
    return beWriteInfoTag( (HBE_STREAM)gfp_save, lpszFileName );
}

修正できたらビルド実行。構成は Release を選ぶ。Debug GTK や Release NASM というものもあるが今回は環境構築が面倒なので使わない。Release でソリューションをリビルドすると lame_vc8.sln と同じ階層にある output フォルダ内に以下のファイルが生成される。

  • lame.exe
  • lame_enc.dll

EXE はアプリケーションとしての LAME で今回は DLL の方を利用する。デスクトップに lib というフォルダを作成して lame_enc.dll とさきほど修正した BladeMP3EncDLL.h をコピーしておく。これで LAME 側の準備は完了。ちなみに lame_enc.dll が公開している API の情報は以下のページが参考になる。

トップページのリンクも含めたかったのだけど言語が分からず (cz ドメインなのでチェコ?)、このページを書いた人と一致するか不明なので保留しておく。あと Homepage 欄の URL 先にもぜんぜん繋がらない。しかし API や BE_CONFIG 構造体を網羅的に扱っているので資料として非常に有用である。

LAME を利用する

Visual Studio で WavToMp3 という MFC ダイアログ アプリを作成。GUI は以下のようにしてみた。

サンプル プログラムの GUI

ソリューションとプロジェクトを作成したら LAME を利用するための設定をおこなう。作成した WavToMp3.sln の階層に前述の lib フォルダを配置、WavToMp3 プロジェクトはそこのヘッダを参照。ビルド終了時に lame_enc.dll を WavToMp3.exe の階層へコピーするように設定しておく。これで LAME を利用できる。

次に LAME の DLL 関数をラップするユーティリティ クラス Lame を作成する。まずはヘッダー。

#pragma once
#include <BladeMP3EncDLL.h>

/**
 * LAME DLL の関数をラップするユーティリティ クラスです。
 */
class Lame
{
public:
    Lame();
    ~Lame();

    static void   Close();
    static BE_ERR CloseStream( HBE_STREAM stream );
    static BE_ERR DeinitStream( HBE_STREAM stream, PBYTE output, PDWORD bytes );
    static BE_ERR EncodeChunk( HBE_STREAM stream, DWORD readBytes, PSHORT input, PBYTE output, PDWORD writeBytes );
    static BE_ERR InitStream( PBE_CONFIG config, PDWORD samples, PDWORD bufferSize, PHBE_STREAM stream );
    static void   Version( PBE_VERSION version );
    static void   WriteVBRHeader( LPCWSTR fileName );
    static BE_ERR WriteInfoTag( HBE_STREAM stream, LPCWSTR fileName );
};

LoadLibrary + GetProcAddress で API を読み込みプログラム終了までキャッシュするような設計にした。そのため API のラッパーと一緒にグローバルなキャッシュを消す Close 関数を宣言。MFC アプリなのでこれは CWinApp 派生クラスの ExitInstance で呼ぶことにする。

次は実装。以下のような関数とクラスをファイル スコープに定義する。

namespace
{
    /**
     * 実行ファイルと同じ階層にあるモジュールのパスを取得します。
     *
     * @param name モジュール名。
     *
     * @return 成功時はパス情報。それ以外は空文字。
     */
    CString GetModulePath( LPCTSTR name )
    {
        TCHAR fullPath[ _MAX_PATH ] = {};
        if( ::GetModuleFileName( ::AfxGetInstanceHandle(), fullPath, _MAX_PATH ) != 0 )
        {
            TCHAR drive[ _MAX_DRIVE ] = {}, dir[ _MAX_DIR ] = {};
            ::_tsplitpath_s( fullPath, drive, _MAX_DRIVE, dir, _MAX_DIR, NULL, 0, NULL, 0 ); 

            CString modPath( drive );
            modPath.AppendFormat( _T( "%s%s" ), dir, name );

            return modPath;
        }

        return CString();
    }

    /**
     * LAME 関数を格納します。
     */
    struct LameFunction
    {
        /**
         * インスタンスを初期化します。
         */
        LameFunction()
        : m_module( NULL )
        , beCloseStream( NULL )
        , beDeinitStream( NULL )
        , beEncodeChunk( NULL )
        , beInitStream( NULL )
        , beVersion( NULL )
        , beWriteVBRHeader( NULL )
        , beWriteInfoTag( NULL )
        {
        }

        /**
         * 初期化を実行します。
         *
         * @return 成功時は true。それ以外は false。
         */
        bool Initialize()
        {
            this->m_module = ::LoadLibrary( GetModulePath( _T( "lame_enc.dll" ) ) );
            if( this->m_module == NULL ) { return false; }

            this->beCloseStream    = reinterpret_cast< BECLOSESTREAM    >( ::GetProcAddress( this->m_module, TEXT_BECLOSESTREAM    ) );
            this->beDeinitStream   = reinterpret_cast< BEDEINITSTREAM   >( ::GetProcAddress( this->m_module, TEXT_BEDEINITSTREAM   ) );
            this->beEncodeChunk    = reinterpret_cast< BEENCODECHUNK    >( ::GetProcAddress( this->m_module, TEXT_BEENCODECHUNK    ) );
            this->beInitStream     = reinterpret_cast< BEINITSTREAM     >( ::GetProcAddress( this->m_module, TEXT_BEINITSTREAM     ) );
            this->beVersion        = reinterpret_cast< BEVERSION        >( ::GetProcAddress( this->m_module, TEXT_BEVERSION        ) );
            this->beWriteVBRHeader = reinterpret_cast< BEWRITEVBRHEADER >( ::GetProcAddress( this->m_module, TEXT_BEWRITEVBRHEADER ) );
            this->beWriteInfoTag   = reinterpret_cast< BEWRITEINFOTAG   >( ::GetProcAddress( this->m_module, TEXT_BEWRITEINFOTAG   ) );

            return true;
        }

        /**
         * 関数の初期化が完了していることを調べます。
         *
         * @return 完了している場合は true。それ以外は false。
         */
        bool IsOpened() const
        {
            return ( this->m_module != NULL );
        }

        /**
         * LAME DLL を解放します。
         */
        void Close()
        {
            if( this->m_module != NULL )
            {
                ::FreeLibrary( this->m_module );
                this->m_module = NULL;
            }
        }

        BECLOSESTREAM    beCloseStream;
        BEDEINITSTREAM   beDeinitStream;
        BEENCODECHUNK    beEncodeChunk;
        BEINITSTREAM     beInitStream;
        BEVERSION        beVersion;
        BEWRITEVBRHEADER beWriteVBRHeader;
        BEWRITEINFOTAG   beWriteInfoTag;

    private:
        HMODULE m_module;
    };

    /**
     * LAME 関数を取得します。
     *
     * @return LAME 関数を格納したオブジェクト インスタンスへの参照。
     */
    LameFunction& GetLameFunction()
    {
        static LameFunction f;
        if( !f.IsOpened() )
        {
            f.Initialize();
        }

        return f;
    }
}

Lame クラスの API ラップ関数では以下のように使用する。

/**
 * エンコード用のストリームを初期化します。
 *
 * @param config     エンコーダの設定。
 * @param samples    サンプル数。
 * @param bufferSize 最小の出力バッファ サイズを返します。
 * @param stream     ストリームのハンドルを返します。
 *
 * @return 処理の成否を示す BE_ERR の値。
 */
BE_ERR Lame::InitStream( PBE_CONFIG config, PDWORD samples, PDWORD bufferSize, PHBE_STREAM stream )
{
    LameFunction& f = ::GetLameFunction();
    return f.beInitStream( config, samples, bufferSize, stream );
}

これを利用する処理は以下のような感じ。

/**
 * WAV ファイルを MP3 に変換します。
 *
 * @param src     変換元となる WAV ファイルへのパス情報。
 * @param dest    変換先となる MP3 ファイルへのパス情報。
 * @param isVBR   可変ビットレートでエンコードするなら true。固定なら false。
 * @param bitrate ビットレート。可変ビットレート時は要素 0 が最小、21 が最大。
 */
bool ConvertWavToMp3( LPCTSTR src, LPCTSTR dest, bool isVBR, int bitrate[] )
{
    HBE_STREAM stream  = 0;
    BE_ERR     error   = BE_ERR_SUCCESSFUL;
    DWORD      sizeWav = 0, sizeMp3 = 0;
    {
        BE_CONFIG config = {};
        config.dwConfig                    = BE_CONFIG_LAME;
        config.format.LHV1.dwStructVersion = 1;
        config.format.LHV1.dwStructSize    = sizeof( config );        
        config.format.LHV1.dwSampleRate    = 44100;
        config.format.LHV1.dwReSampleRate  = 0;
        config.format.LHV1.nMode           = BE_MP3_MODE_STEREO;
        config.format.LHV1.dwBitrate       = bitrate[ 0 ];
        config.format.LHV1.dwMaxBitrate    = ( isVBR ? bitrate[ 1 ] : bitrate[ 0 ] );
        config.format.LHV1.nPreset         = ( isVBR ? LQP_NORMAL_QUALITY : LQP_CBR );
        config.format.LHV1.dwMpegVersion   = MPEG1;
        config.format.LHV1.dwPsyModel      = 0;
        config.format.LHV1.dwEmphasis      = 0;
        config.format.LHV1.bOriginal       = TRUE;
        config.format.LHV1.bWriteVBRHeader = FALSE;
        config.format.LHV1.bNoRes          = TRUE;
        config.format.LHV1.bEnableVBR      = TRUE;

        BE_ERR error = Lame::InitStream( &config, &sizeWav, &sizeMp3, &stream );
        if( error != BE_ERR_SUCCESSFUL ) { return false; }
    }

    FileHandle fileIn( src, _T( "rb" ) );
    if( !fileIn.IsOpened() ) { return false; }

    FileHandle fileOut( dest, _T( "wb+" ) );
    if( !fileOut.IsOpened() ) { return false; }

    // WAV 内のファイル ポインタを data チャンクまで進める
    ::fseek( fileIn, 44, SEEK_SET );

    std::vector< SHORT > bufferWav( sizeWav );
    std::vector< BYTE  > bufferMp3( sizeMp3 );

    // エンコード
    while( ( sizeWav = ::fread( &bufferWav[ 0 ], sizeof( SHORT ), sizeWav, fileIn ) ) > 0 )
    {
        error = Lame::EncodeChunk( stream, sizeWav, &bufferWav[ 0 ], &bufferMp3[ 0 ], &sizeMp3 );
        if( error != BE_ERR_SUCCESSFUL || ::fwrite( &bufferMp3[ 0 ], 1, sizeMp3, fileOut ) != sizeMp3 )
        {
            Lame::CloseStream( stream );
            return false;
        }
    }
    fileIn.Close();

    // エンコード完了
    error = Lame::DeinitStream( stream, &bufferMp3[ 0 ], &sizeMp3 );
    if( error != BE_ERR_SUCCESSFUL || ::fwrite( &bufferMp3[ 0 ], 1, sizeMp3, fileOut ) != sizeMp3 )
    {
        Lame::CloseStream( stream );
        return false;
    }
    fileOut.Close();

    if( isVBR )
    {
        Lame::WriteVBRHeader( dest );
    }
    else
    {
        Lame::WriteInfoTag( stream, dest );
    }

    Lame::CloseStream( stream );

    return true;
}

この処理は LAME に付属する Example.cpp を改造したものである。

CBR/VBR の切り替えとビットレート指定をサポートしてバッファやファイル ハンドル管理などを変更している。詳しくは実際のサンプル プロジェクトを参照のこと。WAV の data チャンク位置は Example.cpp と同様に決めうちとなっているが厳密におこないたいなら mmsystem 系 API でたどった方がいい。

BE_CONFIG.dwConfigBE_CONFIG_MP3 を指定して BE_CONFIG.format.mp3 へ設定すると古いインターフェースを利用したモードになるそうだ。しかしこちらでは VBR が選べないうえ 1 秒未満の WAVE をエンコードするとノイズが出る。前述の資料ページにも OBSOLETE と書かれているので使わないほうがよいだろう。

サンプルを作成するにあたり 5 分ほどの WAVE と Windows の効果音ファイルでエンコードを試してみたが BE_CONFIG_LAME であれば適切な MP3 ファイルを出力できた。

サンプル プロジェクト

今回作成したサンプル プロジェクトを以下に公開する。MFC を利用しているのでビルドには Visual Studio 2010 Professional Edition 以上が必要。

LAME 関連のファイルを再配布するわけにはゆかないので意図的に含めていない。サンプルを試す場合は今回の記事を参考に LAME を自前でビルドし、サンプルの lib フォルダにヘッダとモジュールを配置することになる。DLL 関数をインポートできる環境であれば LAME は C++ 以外でも利用できる。例えば以下のようなサンプルもある。

こちらは C# で DllImport 経由によって LAME を操作。Java で JNIEasy を使った Wrapper プロジェクトもある。

個人的に MP3 はライセンス問題が起きた時点で終わったフォーマットと認識しているのだが、まだ世の中では広く利用されているのでエンコーダ機能を組み込む機会があるかもしれない。そうした時に LAME は有力な選択肢となるはず。

ただし組み込んだプログラムの配布には注意が必要。LAME の配布形態がバイナリでなくソース コードなのも MP3 エンコーダのライセンスを配慮してのこと。今回のサンプルから LAME 関連を外した原因でもある。自前ビルドかつ個人利用にとどめるのが安全だろう。

このあたりの話は「MP3 エンコーダ ライセンス」などでググると様々な見解が見つかるので、一読を推奨する。