Doxygen の使いかた

2012年1月29日 1 ソフト紹介 , , ,

非常に便利なコード ドキュメント作成ツール、Doxygen についての覚え書き。

もくじ

  1. Doxygen とは?
  2. セットアップ
    1. Graphviz
    2. Doxygen
    3. HTML Help Workshop
  3. Doxygen を使ってみる
  4. CHM を生成する

Doxygen とは?

Doxygen とはソースコード ドキュメンテーション ツールである。

ソフトウェア開発をしていると、新参メンバーや外部ユーザー向けの API リファレンスなどを求められる機会がわりとある。そんな時、コードやコメントを自動解析してドキュメント生成してくれると便利である。Doxygen はそのような要求を叶えてくれる。

この種のツールでは Java の Javadoc や.NET の NDocSandcastle などが有名である。これらが特定のプログラミング言語やプラットフォームに最適化されているのに対して Doxygen は総合的である。多数の言語に対応し Windows、Mac、Linux 上で動作する。

ドキュメントの出力形式も豊富である。HTML を基本とし、LaTeX、XML、CHM ( HTML ヘルプ )、PDF など様々な形式をサポートしている。

これだけ多機能にもかかわらずフリーウェアである。現時点で十分に有用だが常に改善され続けている。実にありがたい。

インストール

今回は Windows 7 64bit 環境にインストールする。32bit だったり Windows XP などでも手順はそれほど変わらないはず。

Graphviz

ではさっそく Doxygen のインストール!とゆきたいところだが、先に Graphviz を入れる。これは DOT というスクリプト言語によってグラフ生成するためのツール。Doxygen はクラス ダイアグラムを生成する機能を持っているのだが、そこで Graphviz を利用する。

現行の Doxygen なら大丈夫かもしれないが、昔のバージョンでは先に Graphviz を入れないと手動設定が必要だった。そのため Graphviz からインストールする。

Graphviz のダウンロード ページにゆくと様々なプラットフォーム用リンクがあるが、今回は Windows 環境にインストールするので Stable and development Windows Install packages を選ぶ。

ページへのアクセスが初めての場合、ページ最下段に使用許諾のテキストと共に Agree/Disagree というリンクが表示される。内容を確認したうえで Agree を選ぶ。するとインストーラのページに移動するので graphviz-2.28.0.msi ( 2012/1/29 現在の最新 ) をクリックしてダウンロードを開始する。

セットアップについては特に迷うところもないため割愛。

Doxygen

Doxygen のインストール。

上記ダウンロードページの A 32-bit binary distribution for Windows XP/Vista/7 という欄にある doxygen-1.7.6.1-setup.exe の横にインストーラのリンクがある。プロトコルに FTP/HTML を選べる。特にこだわりがなければ HTTP でよい。

Doxygen のセットアップも、特に困るような箇所はない。

HTML Help Workshop

Doxygen で CHM ( HTML ヘルプ ) 形式のドキュメントを生成したい場合は HTML Help Workshop もインストールしておく。

上記ダウンロード ページにゆくと 3 種類のリンクがある。これらから htmlhelp.exe を選ぶ。HelpDocs.zip と htmlhelpj.exe はツールに関するドキュメントなので CHM 生成だけなら不要。

ダウンロードした htmlhelp.exe を実行するとセットアップが開始される。Doxygen からこのツールを利用するとき実行ファイルとなる hhc.exe のパスが必要になるためインストール先をたずねられたらパスをメモしておくとよい。

Doxygen を使ってみる

Doxygen のセットアップが完了したら Doxywizard を起動する。これは Doxygen の設定・実行用 GUI ツールである。Doxygen の設定ファイルはテキストエディタでも編集可能だが面倒だしタイプミスも怖いので、特別な理由がない限り Doxywizard を利用するのがよいと思う。

というわけで Doxywizard を使ってみよう。スタートメニューから「doxygen」→「Doxywizard」を選ぶか、コマンドプロンプト上で doxywizard とタイプして Enter キーを押すことでツールが起動される。

Doxywizard

画面上の説明どおりに設定してゆく。

まずは最上段から Doxygen の作業フォルダを指定しよう。Doxygen 以外のファイルやフォルダが存在しない場所を選ぶとよい。画面中段から下が設定部分となる。タブでモードを切り替えられる。それぞれの役割は以下のようになる。

項目 内容
Wizard 簡易設定。ウィザード形式で Project ~ Diagrams まで設定してゆく。
Expert 高度な設定。Wizard より細かな設定をおこなえる。
Run ドキュメント生成などを実行できる

作業用フォルダを設定したら Wizard タブを開く。左がウィザード設定ページの選択、右側は設定となる。ウィザードは Project から始まる。

Project

Project ページ。プロジェクト全般の設定をおこなう。項目は以下。

項目 内容
Project name プロジェクト名。ドキュメントのタイトルなど様々な場所に反映される。
Project synopsis プロジェクト概要。出力形式が HTML の場合、タイトル直下に説明文として添えられる。
Project version or id プロジェクトのバージョン情報、または識別子。出力形式が HTML の場合、タイトル右側に添えられる。
Project logo プロジェクトのロゴとなる画像ファイル。出力形式が HTML の場合、タイトル左側に画像として挿入される。
Select code directory 解析対象とするソースコードを格納したフォルダの場所。相対パスも指定できる。
Scan recursively ソースコードのフォルダが階層化されており、それらも解析する場合はチェックする。
Destination directory ドキュメントの出力先。未指定なら作業フォルダが選ばれる。

ひととおり設定したら Next ボタンを押して次へ。

Mode

Mode ページ。ソースコードの解析方法に関する設定をおこなう。項目は以下。

項目 内容
Select the desired extraction mode 解析対象。Documented entities only はドキュメント化されたファイル ( コメントが含まれるということだろうか? ) のみ。All Entities ならすべて。Include cross-referenced はソース間で相互参照がある場合、それらも含む。
Select programming language to optimize the result for 最適化するプログラミング言語。プロジェクトのメイン言語を選ぶ。

ひととおり設定したら Next ボタンを押して次へ。

Output

Output ページ。ドキュメントの出力形式に関する設定をおこなう。設定項目は以下。

HTML

項目 内容
plain HTML 標準の HTML を出力する。
with navigation panel ページの左側にナビゲーション パネルを持つ HTML を出力する。
prepare for compressed HTML(.chm) Microsoft Compiled HTML Help ( CHM ) 形式で出力する。
With search function PHP の稼働している Web サーバーに配置したとき、検索機能を有効にする。
Change color… HTML 出力する際の全体的な色あいを変更できる。既定のデザインを踏襲しつつ、色のみを調整するようになっている。

LaTeX

項目 内容
as intermediate format for hyperlinked PDF LaTeX 用の中間形式としてハイパーリンク形式の PDF を出力する。
as intermediate format for PDF LaTeX 用の中間形式として単一の PDF を出力する。
as intermediate format for PostScript LaTeX 用の中間形式としてポストスクリプト ファイルを出力する。

その他

項目 内容
Man pages UNIX の man ( マニュアル ) コマンド用ファイルを出力する。
Rich Text Format(RTF) リッチテキスト形式のファイルを出力する。
XML XML 形式のファイルを出力する。独自の出力形式が欲しい場合、このファイルから変換するのがよさそう。

ひととおり設定したら Next ボタンを押して次へ。

Diagrams

Diagrams ページ。ダイアグラムの出力設定をおこなう。項目は以下。

Diagram to generate

項目 内容
No diagrams ダイアグラムを出力しない。
Use built-in class diagram generator Doxygen に内蔵されてるジェネレータを利用してダイアグラムを出力する。
Use dot tool from the GraphViz package to generate GraphViz を利用してダイアグラムを出力する。詳細設定は Dot graphs to generate 欄でおこなう。

Dot graphs to generate

項目 内容
Class diagrams クラス図を作成する。
Collaboration diagrams コラボレーション図を作成する。
Include dependency graphs インクルード依存グラフを作成する。
Include by dependency graphs インクルード依存グラフを作成する。INCLUDED_BY_GRAPH の説明を読むに、ヘッダの間接インクルードによって生じた依存関係も含むようだ。
Call graphs 呼び出しグラフを作成する。これをチェックすると関数やメソッドの呼び出し解析が実行されるので、ドキュメント生成の時間がとても長くなる。
Called by graphs 間接的な呼び出しフラグを作成する。この設定も Call graphs 同様、チェックすると時間がかかる。

Wizard タブでおこなう設定は以上。出力される HTML 上のメニュー項目などを日本語化したいなら Expert タブを開き OUTPUT_LANGUAGE 欄を English から Japanese に変更しておく。

言語の設定

すべての設定が完了したら Run タブを開いて Run doxygen ボタンを押す。しばらく待つとドキュメント作成が完了する。

ドキュメント生成の実行

生成されたドキュメントが HTML なら index.html がトップページとなる。表示すると以下のようになる。

生成された HTML

出力された内容が妥当なら Doxywizard のメインメニューから「File」→「Save」または「Save as…」を選んで設定を記録しておく。ファイル名や拡張子は何でもよい。標準では Doxyfile という名前になる。保存先もお好みで。私は Doxygen の作業用フォルダ直下に標準の名前で保存している。

CHM を生成する

Doxygen で Microsoft Compiled HTML Help ( CHM ) 形式、いわゆる CHM ファイルを生成する方法についてまとめる。

CHM の生成には HTML Help Workshop が必要なので、もしインストールしていないなら本記事の HTML Help Workshop 欄を参考にセットアップしておく。完了したらインストール先の hhc.exe までのフルパスをメモする。

次に Doxywizard を起動して CHM ファイルを出力したいプロジェクトの Doxygen 設定ファイルを開く。続けて Expert タブ内の HTML を選び GENERATE_HTMLHELP 以降を以下のように設定する。

CHM 関連の設定

項目 内容
GENERATE_HTMLHELP チェックすることで、CHM ファイルの出力が有効になる。
CHM_FILE 出力するファイル名。プロジェクト名.chm とするのが分かりやすくてよいだろう。
HHC_LOCATION HTML Help Workshop の実行ファイルへのフルパス。メモっておいたパスを入力するか、隣のボタンを押すと表示されるファイル ダイアログで hhc.exe を開く。
CHM_INDEX_ENCODING CHM ファイルを開いた時、目次タブに表示される項目のテキスト エンコーディング。日本語が含まれるなら Shift_Jis を設定する。そうしないと文字化けする。

環境と設定が適切ならドキュメント出力を実行することで CHM ファイルが生成されるはず。ファイルを開くと以下のような感じになる。

生成された CHM

キーワードによる絞り込みや検索も有効なので、かなり便利。リファレンスを配布する相手が Windows ユーザーなら検討してみる価値のある形式である。

LAME で MP3 エンコード

2011年3月20日 1 開発 , , , ,

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

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

もくじ

LAME の入手とビルド

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

現時点の最新バージョンは 3.98.4 となる。上記ページのリンク先に lame-3.98.4.tar.gz というファイルが公開されているので、ダウンロードした後に展開する。

展開されたフォルダ内には lame_vc8.sln というファイルがあるので、これを Visual Studio 2010 で開き、変換ウィザードで 2010 用にする。mp3x_vc8 というプロジェクトでエラーになるけれど、これは不要なので気にしなくてもよい。

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 を _wfopen_s にしておく。他にも直したい箇所は数多くあるのだが、とりあえずは限定的な変更にとどめておく。

__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 ラップ関数では以下のように使用する。

<br />/**
 * エンコード用のストリームを初期化します。
 *
 * @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.dwConfig に BE_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 エンコーダ ライセンス」などでググると様々な見解が見つかるので、一読を推奨する。

JavaScript だけで EXIF を読む 2

2010年5月5日 2 開発 , ,

先週、JavaScript だけで EXIF を読むという記事を書いたのだが、万里小路さんのコメントにより、サンプルとして公開していたスクリプトにバグがあることが判明した。

具体的には、文字列値として埋め込まれているデータをそのまま読み込むため、NULL 文字などが入っていると文字化けが発生する。スクリプトの元ネタである Jacob Seidelin 氏のスクリプトでもこの部分の処理は一緒なので、同じ問題が起きると思う。

問題の処理は binaryajax.js にあり、以下のように実装されている。

this.getStringAt = function( offset, length )
{
  var value = [];
  for( var i = offset, j = 0; i < offset + length; ++i, ++j )
  {
    value[ j ] = String.fromCharCode( this.getByteAt(
    i) );
  }

  return value.join( "" );
};

文字列値として取得されるデータは、文字コードが埋め込まれている。6 行目の this.getByteAt( i ) という部分は整数値を返し、 String.fromCharCode() はそれを文字化する。データの文字列化という観点から見ると正しい処理なのだが、NULL 文字のように表示不能なものまで含むので、人間が読むものとしては不適切である。

そこで、以下のように変更してみた。

this.getStringAt = function( offset, length )
{
  var value = [];
  var max   = offset + length;
  for( var i = offset, j = 0; i < max; ++i, ++j )
  {
    // 0x00 ~ 0x1F は制御コードなので無視する
    var data = this.getByteAt( i );
    if( data < 32 ) { continue; }

    value[ j ] = String.fromCharCode( data );
  }

  return value.join( "" );
};

重要な変更点は 7 ~ 9 行目となる。文字コードで 0x00 ~ 0x1F は制御コードとなり、表示できないと思われるので無視するようにした。この変更を行ったスクリプトとサンプルを以下に公開する。ライセンスは前回のサンプルと同様に Mozilla Public License とする。

備考

MP3 の ID3v2 タグなどもそうだが、ファイルの先頭や途中に記述するタイプのメタデータは、データの更新が発生するたびにファイル全体の書き換えが必要となるので、効率化のために以下のような処理をおこなうことがある。

  • メターデータの領域にパディング ( 余白 ) をもたせておく
  • 余白には無効値として NULL を入れる
  • データが文字列値の場合、一般的な処理系ならば自動的に文字列終端として切り捨てられる
  • データが数値でも、文字列処理すれば余白がなくなるので、それを数値化すればよい。数値系メタデータの場合、たいがいは固定幅だと思われるが…
  • データの書き換えが発生したとき、データ長がパディングの範囲内なら他の部分の伸長は発生しないので、部分更新で済む

前回の記事のコメントでは、Cannon のカメラで撮影した写真の Model などに NULL 文字が入っているとのことだった。推測になるが、これは上記のようなパディングとして入れているのではなかろうか?