Programming Field - プログラミング Tips

libmp3lameのAPIを使ったデコード

LAMEはオープンソースのMP3エンコーダー/デコーダーを含むソフトウェア(ライブラリ)です。マルチメディアファイルを再生する多くのアプリケーションで用いられています。

ここでは、LAMEに含まれている「libmp3lame」ライブラリを使ってMP3をデコードし、WAVEデータとして再生する手順を簡単に紹介します。

準備

まずLAMEライブラリをビルドし、生成物からlibmp3lameのライブラリ(Windowsの場合は「libmp3lame.dll」と「libmp3lame.lib」)と「lame.h」を取り出します。この手順に関しては省略しますが、付属ドキュメント「INSTALL」に紹介されているのでそちらを参照してください。

初期化

デコードの関数を使うには、「hip_decode_init」関数を呼び出してhip_t型のデータ(以降便宜上「hipハンドル」と呼びます)を取得します。また、サンプルデータのスキップを計算する場合は「lame_init」を呼び出してその戻り値を保持しておきます。

    lame_global_flags* lgf;
    hip_t hip;

    lgf = lame_init();
    if (!lgf)
    {
        perror("lametest: init_lame_buffers: lame_init");
        return -1;
    }
    /* init decoding */
    hip = hip_decode_init();
    if (!hip)
    {
        perror("lametest: init_lame_buffers: hip_decode_init");
        lame_close(lgf);
        return -1;
    }

MP3ヘッダーの解析

hipハンドルを取得したら解析を始めます。ただし、MP3ファイルの先頭にID3データが含まれている場合は、解析の前にそれらを読み飛ばします。

    FILE* pFile;
    void* pvBuffer;

    size_t nSize, nPos;
    size_t headerSize;

    nSize = fread(pvBuffer, 1, FILE_BUFFER_SIZE, pFile);
    if (nSize < 4)
    {
        if (!nSize)
            perror("lametest: fread");
        else
            fprintf(stderr, "lametest: too small file size");
        return -1;
    }

    nPos = 0;
    /* check if the file has ID3 header */
    if (memcmp(pvBuffer, "ID3", 3) == 0)
    {
        if (nSize < 10)
        {
            nPos = fread(((unsigned char*) pvBuffer) + nSize, 1, 10 - nSize, pFile);
            if (nPos + nSize < 10)
            {
                if (!nPos)
                    perror("lametest: fread");
                else
                    fprintf(stderr, "lametest: too small file size");
                return -1;
            }
            nSize += nPos;
        }
        /* retrieve size of ID3 data */
        headerSize = ((size_t)(((unsigned char*) pvBuffer)[6] & 0x7F) << 21) |
            ((size_t)(((unsigned char*) pvBuffer)[7] & 0x7F) << 14) |
            ((size_t)(((unsigned char*) pvBuffer)[8] & 0x7F) << 7) |
            (size_t)(((unsigned char*) pvBuffer)[9] & 0x7F);
        /* if entire ID3 data has been read, then set current position */
        if (nSize - 10 >= headerSize)
        {
            nPos = 10 + headerSize;
            nSize -= nPos;
        }
        else
        {
            /* skip existing ID3 data */
            while (headerSize)
            {
                if (headerSize > FILE_BUFFER_SIZE)
                    nSize = FILE_BUFFER_SIZE;
                else
                    nSize = headerSize;
                nPos = fread(pvBuffer, 1, nSize, pFile);
                if (!nPos)
                {
                    perror("lametest: fread");
                    return -1;
                }
                headerSize -= (int) nPos;
            }
            if (!headerSize)
            {
                nPos = 0;
                nSize = 0;
            }
        }
    }

データをスキップしたら、hip_decode1_headersB関数を使ってヘッダー情報などを取得します。(サンプルデータのスキップを計算しない場合はhip_decode1_headers関数を使います。)

    short buffL[1152], buffR[1152];     /* max count of samples per frame is 1152 */
    int decodeCount, nEncDelay, nEncPadding;
    mp3data_struct mp3data;

    decodeCount = -1;
    nEncDelay = nEncPadding = -1;
    mp3data.header_parsed = 0;
    while (1)
    {
        if (nSize)
        {
            /* retrieve the MP3 file info */
            decodeCount = hip_decode1_headersB(hip, (unsigned char*) pvBuffer + nPos, nSize,
                buffL, buffR, &mp3data, &nEncDelay, &nEncPadding);

            if (decodeCount < 0)
            {
                perror("lametest: hip_decode1_headersB");
                break;
            }
            nPos = 0;
            if (mp3data.header_parsed)
                break;
        }
        nSize = fread(pvBuffer, 1, FILE_BUFFER_SIZE, pFile);
        if (!nSize)
        {
            perror("lametest: fread");
            break;
        }
    }
    if (decodeCount < 0)
        return -1;

WAVE出力の準備

※ ここからWindows特有のコードを含みます。

取得したヘッダー情報を元にWAVEFORMATEX構造体を初期化します。

    WAVEFORMATEX* pwfx;

    pwfx = (WAVEFORMATEX*) malloc(sizeof(WAVEFORMATEX));
    if (!pwfx)
    {
        perror("lametest: init_wave_format_ex: malloc[1]");
        return -1;
    }

    /* build WAVEFORMATEX data from MP3 info */
    pwfx->wFormatTag = WAVE_FORMAT_PCM;
    pwfx->nChannels = (WORD) mp3data.stereo;
    pwfx->nSamplesPerSec = (DWORD) mp3data.samplerate;
    pwfx->wBitsPerSample = 16;
    pwfx->nBlockAlign = pwfx->nChannels * pwfx->wBitsPerSample / 8;
    pwfx->nAvgBytesPerSec = pwfx->nSamplesPerSec * pwfx->nBlockAlign;
    pwfx->cbSize = 0;

初期化したら、この構造体を使ってWAVEデバイスをオープンします。

    MMRESULT mr;
    HWAVEOUT hWaveOut;
    size_t n;

    /* open wave device */
    mr = waveOutOpen(&hWaveOut, WAVE_MAPPER, pwfx, 0, 0, CALLBACK_NULL);
    if (mr != MMSYSERR_NOERROR)
    {
        wave_perror(mr, "lametest: init_wave_format_ex: waveOutOpen");
        free((void*) pwfx);
        *ppwfx = NULL;
        *phWaveOut = NULL;
        return -1;
    }

さらに、出力用のバッファのメモリを割り当て、waveOutPrepareHeaderを使って初期化します。

    size_t hdrCount;
    WAVEHDR* pwavehdr;  /* must be allocated (= malloc(sizeof(WAVEHDR) * hdrCount)) */

    /* init WAVEHDR data */
    memset(pwavehdr, 0, sizeof(WAVEHDR) * hdrCount);
    for (n = 0; n < hdrCount; n++)
    {
        pwavehdr[n].lpData = (LPSTR) malloc(WAVE_BUFFER_SIZE);
        if (!pwavehdr[n].lpData)
        {
            perror("lametest: init_wave_format_ex: malloc[2]");
            while (n--)
            {
                waveOutUnprepareHeader(hWaveOut, &pwavehdr[n], sizeof(WAVEHDR));
                free((void*) pwavehdr[n].lpData);
                pwavehdr[n].lpData = NULL;
            }
            waveOutClose(hWaveOut);
            free(pwfx);
            return -1;
        }
        mr = waveOutPrepareHeader(hWaveOut, &pwavehdr[n], sizeof(WAVEHDR));
        if (mr != MMSYSERR_NOERROR)
        {
            wave_perror(mr, "lametest: init_wave_format_ex: waveOutPrepareHeader");
            free(pwavehdr[n].lpData);
            while (n--)
            {
                waveOutUnprepareHeader(hWaveOut, &pwavehdr[n], sizeof(WAVEHDR));
                free((void*) pwavehdr[n].lpData);
                pwavehdr[n].lpData = NULL;
            }
            waveOutClose(hWaveOut);
            free(pwfx);
            return -1;
        }
    }

デコードとデータの出力

準備が整ったらデータをデコードしてそれを出力します。なお、ヘッダーを解析した際に既に一部データがデコードされているので、先にそのデータを出力するように処理を書きます。

データのデコードはhip_decode1関数を用います。デコードする際、以前の呼び出し時に使用されなかったデータが内部で保持されている場合があるため、それを取得するには入力データサイズを0にして呼び出す必要があります。

データを取得したら出力用バッファに転送し、それをwaveOutWrite関数を使ってデバイスに転送します。

※「hip_decode」関数も利用できますが、こちらは1フレーム単位ではないため、取得に必要なバッファのサイズを計算する手間が発生します。「hip_decode1」関数は1フレーム単位でデータが返るため、必要なバッファサイズは最大でも1152(×2(L-R)×2(16ビット))となります。

/* copies samples to buffer */
static size_t frame_buffer_to_wave_buffer(int frameCount, int stereo,
    const short* buffL, const short* buffR, void* pvBuffer, size_t _buffer_size)
{
    short* p;
    size_t n;

    /* check buffer size */
    if (_buffer_size < ((size_t) frameCount) * 2 * stereo)
    {
        _set_errno(EINVAL);
        return 0;
    }
    p = (short*) pvBuffer;
    n = 0;
    while (frameCount--)
    {
        *p++ = *buffL++;
        n += sizeof(short);
        if (stereo)
        {
            *p++ = *buffR++;
            n += sizeof(short);
        }
    }
    return n;
}


    int hdrPos, nowSample;

    hdrPos = 0;
    nowSample = 0;
    while (!s_nInterrupted)
    {
        nSize = 1;
        /* check whether the decoded data is not available */
        if (!decodeCount)
        {
            /* flush buffers that is not decoded */
            decodeCount = hip_decode1(hip, (unsigned char*) pvBuffer, 0,
                buffL, buffR);
            while (!decodeCount)
            {
                /* there is no buffer left, so read from file again */
                nSize = fread(pvBuffer, 1, FILE_BUFFER_SIZE, pFile);
                if (!nSize)
                {
                    /* file is EOF, so wait until finishing the play */
                    while (!(wavehdr[hdrPos].dwFlags & WHDR_DONE) &&
                        (wavehdr[hdrPos].dwFlags != WHDR_PREPARED))
                    {
                        if (s_nInterrupted)
                            break;
                        my_sleep(1);
                    }
                    if (!s_nInterrupted)
                    {
                        fprintf(stderr, "Finished.\n");
                        exitCode = 0;
                    }
                    break;
                }
                /* decode */
                decodeCount = hip_decode1(hip, (unsigned char*) pvBuffer, nSize,
                    buffL, buffR);
                if (decodeCount < 0)
                {
                    perror("lametest: hip_decode1[2]");
                    break;
                }
            }
        }
        if (!nSize || decodeCount < 0)
            break;
        /* skip first samples */
        if (skipSamples)
        {
            if (decodeCount < skipSamples)
            {
                skipSamples -= decodeCount;
                continue;
            }
            memmove(&buffL[0], &buffL[skipSamples], sizeof(short) * (decodeCount - skipSamples));
            memmove(&buffR[0], &buffR[skipSamples], sizeof(short) * (decodeCount - skipSamples));
            decodeCount -= skipSamples;
            skipSamples = 0;
        }
        /* skip last samples */
        if (nEncPadding > 0 && feof(pFile))
        {
            if (decodeCount < nEncPadding)
                decodeCount = 0;
            else
                decodeCount -= nEncPadding;
        }

        if (nowSample)
            tty_move_lineup(stderr, 1);
        nowSample += decodeCount;
        fprintf(stderr, "now sample = %d\n", nowSample);

        /* set frame buffer to output buffer */
        nSize = frame_buffer_to_wave_buffer(decodeCount, mp3data.stereo,
            buffL, buffR, (void*) pwavehdr[hdrPos].lpData, WAVE_BUFFER_SIZE);
        if (!nSize)
        {
            perror("lametest: _frame_buffer_to_wave_buffer_s");
            break;
        }

        /* output buffer to device */
        wavehdr[hdrPos].dwBufferLength = (DWORD) nSize;
        mr = waveOutWrite(hWaveOut, &pwavehdr[hdrPos], sizeof(WAVEHDR));
        if (mr != MMSYSERR_NOERROR)
        {
            wave_perror(mr, "lametest: waveOutWrite");
            break;
        }

        /* use next buffer */
        decodeCount = 0;
        hdrPos++;
        if (hdrPos == hdrCount)
            hdrPos = 0;

        /* wait until the buffer can use */
        while (!(pwavehdr[hdrPos].dwFlags & WHDR_DONE) &&
            (pwavehdr[hdrPos].dwFlags != WHDR_PREPARED))
        {
            if (s_nInterrupted)
                break;
            my_sleep(1);
        }
    }

後処理

今まで使ったデータをすべて解放します。

    while (hdrCount--)
    {
        if (pwavehdr[hdrCount].lpData)
        {
            waveOutUnprepareHeader(hWaveOut, &pwavehdr[hdrCount], sizeof(WAVEHDR));
            free((void*) pwavehdr[hdrCount].lpData);
        }
    }
    if (hWaveOut)
        waveOutClose(hWaveOut);
    if (pwfx)
        free((void*) pwfx);

    if (hip)
        hip_decode_exit(hip);
    if (lgf)
        lame_close(lgf);

サンプルプログラム

以上のコードを使ったサンプルプログラム(一部修正)は以下のリンクからダウンロードできます。

最終更新日: 2011/12/18