L-Smash を使って無圧縮動画を作成する

## L-Smash とは何か

[L-Smash](https://github.com/l-smash/l-smash) とは mp4 及び mov コンテナを読み書きできる C99 で書かれたライブラリである。名前で見て気づいた人はわかると思うが、動画編集アプリケーションのひとつ [AviUtl](http://spring-fragrance.mints.ne.jp/aviutl/) において事実上必須となっている [L-Smash Works](https://pop.4-bit.jp/?page_id=7929) の土台となっている。

あくまでコンテナを読み書きするライブラリなので、音声や動画のエンコード及びデコードはできないが後述の方法により L-Smash ライブラリ単体で無圧縮動画を作成することが可能である。

## どうやって格納するか

L-Smash は mp4 及び mov コンテナ両方対応しているが、そのうち無圧縮動画を作成できるのは mov コンテナなので、mov コンテナを使って動画形式を RGB24 (RGB それぞれ 8bit で格納) かつ音声形式を LPCM (Linear PCM) で入れることにする。

## L-Smash を使って無圧縮動画を作成する

以下 L-Smash を使って無圧縮動画を作成する方法を手順を踏む形で解説する。解説で使う言語は C++ (C++11) であるが、C++ でなければならない要素はなく、C++ 以外の言語、たとえば Swift や Kotlin Native でも原理的には応用できるとみている。

### 各構造体を初期化

`lsmash_root_t` は外向き中身の見えない構造体のため、 `lsmash_create_root` で初期化する。
以下の構造体は専用の初期化関数があるのでそれで初期化する

- `lsmash_movie_parameters_t`
- `lsmash_track_parameters_t`
- `lsmash_media_parameters_t`

`lsmash_file_t` はあとで初期化するためここでは `nullptr` を指定する。

```c++
lsmash_file_t *file = nullptr;
lsmash_root_t *root = lsmash_create_root();
lsmash_movie_parameters_t movieParameters;
lsmash_track_parameters_t audioTrackParameters;
lsmash_track_parameters_t videoTrackParameters;
lsmash_media_parameters_t audioMediaParameters;
lsmash_media_parameters_t videoMediaParameters;
lsmash_initialize_movie_parameters(&movieParameters);
lsmash_initialize_track_parameters(&audioTrackParameters);
lsmash_initialize_track_parameters(&videoTrackParameters);
lsmash_initialize_media_parameters(&audioMediaParameters);
lsmash_initialize_media_parameters(&videoMediaParameters);
```

### トラック有効化

各トラックを有効化する。動画トラックには `ISOTRACK_IN_MOVIE` を付加する。`movieParameters.timescale` は動画のフレームレートを指定する。

```c++
uint32_t audioTrackFlags = ISOTRACK_ENABLED;
uint32_t videoTrackFlags = ISOTRACK_ENABLED | ISOTRACK_IN_MOVIE;
// ここでは 60FPS とする
movieParameters.timescale = 60;
audioTrackParameters.mode = static_cast<lsmash_track_mode>(audioTrackFlags);
videoTrackParameters.mode = static_cast<lsmash_track_mode>(videoTrackFlags);
```

### `lsmash_file_parameters_t` を初期化

I/O 関連及びコールバックを管理する構造体であるが、今回は特にコールバック指定は行わずファイル上でやりとりする。原理的にはコールバック指定を用いてメモリ上で読み書きすることが可能。

```c++
lsmash_file_parameters_t fileParameters;
memset(&fileParameters, 0, sizeof(fileParameters));
fileParameters.mode = LSMASH_FILE_MODE_WRITE;
fileParameters.major_brand = ISOBRAND_TYPE_QT;
```

### `lsmash_audio_summary_t` を初期化

`lsmash_create_summary` を呼び出して各種パラメータを設定する。 `lsmash_create_summary` は返る値の型が `lsmash_summary_t` なので、`lsmash_audio_summary_t` にアップキャストしている。

mov コンテナかつ LPCM の場合は `lsmash_qt_audio_format_specific_flags_t` の設定が必要なので、 `lsmash_create_codec_specific_data` を使って`lsmash_qt_audio_format_specific_flags_t` を設定して `lsmash_add_codec_specific_data` で追加する。

`lsmash_qt_audio_format_specific_flags_t` の設定がないとサンプル格納時にエラーになる

```c++
lsmash_audio_summary_t　*audioSummary = reinterpret_cast<lsmash_audio_summary_t *>(lsmash_create_summary(LSMASH_SUMMARY_TYPE_AUDIO));
audioSummary->sample_type = QT_CODEC_TYPE_LPCAUDIO;
// ここではサンプルレート 44100Hz チャンネル数 2 サンプルビット数 16 を指定
audioSummary->frequency = 44100;
audioSummary->channels = 2;
audioSummary->sample_size = 16;
{
    lsmash_codec_specific_t *spec = lsmash_create_codec_specific_data(LSMASH_CODEC_SPECIFIC_DATA_TYPE_QT_AUDIO_FORMAT_SPECIFIC_FLAGS, LSMASH_CODEC_SPECIFIC_FORMAT_STRUCTURED);
    lsmash_qt_audio_format_specific_flags_t *format = static_cast<lsmash_qt_audio_format_specific_flags_t *>(spec->data.structured);
    uint32_t flags = QT_AUDIO_FORMAT_FLAG_SIGNED_INTEGER | QT_AUDIO_FORMAT_FLAG_PACKED;
    format->format_flags = static_cast<lsmash_qt_audio_format_specific_flag>(flags);
    lsmash_add_codec_specific_data(reinterpret_cast<lsmash_summary_t *>(audioSummary), spec);
    lsmash_destroy_codec_specific_data(spec);
}
```

### `lsmash_video_summary_t` を初期化

`lsmash_audio_summary_t` と同じように `lsmash_create_summary` を呼び出して各種パラメータを設定する。

```c++
lsmash_video_summary_t　*videoSummary = reinterpret_cast<lsmash_video_summary_t *>(lsmash_create_summary(LSMASH_SUMMARY_TYPE_VIDEO));
videoSummary->sample_type = QT_CODEC_TYPE_RAW_VIDEO;
videoSummary->depth = QT_VIDEO_DEPTH_COLOR_24;
// ここでは幅 960 高さ 640 で指定する
videoSummary->width = 960;
videoSummary->height = 640;
```

### ファイルを開く

準備ができたので `lsmash_open_file` を使ってファイルを開く。

`lsmash_create_track` と `lsmash_add_sample_entry` をそれぞれ呼び出し、トラック及びメディアパラメータを設定する。`lsmash_add_sample_entry` が返すサマリインデックスはサンプル追加に必須なので保持する。

`videoMediaParameters.timescale` は `movieParameters.timescale` をそのまま割り当てる。`audioSummary->samples_in_frame` は音源の周波数 (= `audioSummary->frequency`) を動画のフレームレート (= `movieParameters.timescale`) で割った値を、`audioMediaParameters.timescale` は音源の周波数を指定する。

```c++
uint32 audioTrackID = 0, videoTrackID = 0, audioSummaryIndex = 0, videoSummaryIndex = 0;
int result = lsmash_open_file(path, 0, &fileParameters);
// 成功時は 0 を返す
if (result == 0) {
    file = lsmash_set_file(root, &fileParameters);
    // 上のコードでは既定値を指定しているためここでの分岐は無意味だが、もし音源が必要ない場合は追加させないようにする必要がある。
    // 音源がないのに音源のエントリを追加すると書き出しでエラーになるため。
    if (audioSummary->frequency > 0 && audioSummary->channels > 0 && audioSummary->sample_size > 0) {
        audioSummary->samples_in_frame = audioSummary->frequency / movieParameters.timescale;
        audioMediaParameters.timescale = audioSummary->frequency;
        audioTrackID = lsmash_create_track(root, ISOMEDIA_HANDLER_TYPE_AUDIO_TRACK);
        audioSummaryIndex = lsmash_add_sample_entry(root, audioTrackID, audioSummary);
        lsmash_set_track_parameters(root, audioTrackID, &audioTrackParameters);
        lsmash_set_media_parameters(root, audioTrackID, &audioMediaParameters);
    }
    videoMediaParameters.timescale = movieParameters.timescale;
    videoTrackID = lsmash_create_track(root, ISOMEDIA_HANDLER_TYPE_VIDEO_TRACK);
    videoSummaryIndex = lsmash_add_sample_entry(root, videoTrackID, videoSummary);
    lsmash_set_movie_parameters(root, &movieParameters);
    lsmash_set_track_parameters(root, videoTrackID, &videoTrackParameters);
    lsmash_set_media_parameters(root, videoTrackID, &videoMediaParameters);
}
```

### オーディオフレームの追加

以下の手順を踏む形でフレームを追加する。オーディオの場合フレームは `audioSummary->samples_in_frame` 分のサンプルを持ったデータとみなす。

- `lsmash_create_sample` を使ってサンプルを作成
- `lsmash_sample_alloc` でサンプル格納に必要なデータを確保
- `memcpy` でサンプルデータをコピー
- `sample->cts` および `sample->dts` に `current_frame_index` の `audioSummary->samples_in_frame` をかけた値を代入
- `sample->index` に `summaryIndex` を設定する
  - 設定し忘れると `lsmash_append_sample` でエラーになる
- `lsmash_append_sample` でサンプルをコンテナに追加

外部から渡す必要のある引数は以下

- data
  - `audioSummary->samples_in_frame` のサンプルを持つフレームのデータ
  - 16bit 符号ありのリトルエンディアンである必要がある
- size
  - フレームデータのサイズ
  - `audioSummary->samples_in_frame * audioSummary->channels * (audioSummary->sample_size / 8)` でなければならない
- current_frame_index
  - 現在のフレーム位置

```c++
lsmash_sample_t *sample = lsmash_create_sample(size);
lsmash_sample_alloc(sample, size);
memcpy(sample->data, data, size);
sample->cts = sample->dts = current_frame_index * audioSummary->samples_in_frame;
sample->index = audioSummaryIndex;
int result = lsmash_append_sample(root, audioTrackID, sample);
if (result != 0) {
    // エラー処理
}
```

### ビデオフレームの追加

オーディオフレームの追加と以下の違いがあることを除いて基本的に同じ。ビデオにおいてフレームはサンプルと同一。

- `cts` 及び `dts` には `current_frame_index` を直接代入する
- `index` には `videoSummaryIndex` を入れる

外部から渡す必要のある引数は以下

- data
  - フレームデータ
  - `RGB` 形式で並ぶ必要がある
- size
  - フレームデータのサイズ
  - `videoSummary->width * videoSummary->height * 3` でなければならない
- current_frame_index
  - 現在のフレーム位置

```c++
lsmash_sample_t *sample = lsmash_create_sample(size);
lsmash_sample_alloc(sample, size);
memcpy(sample->data, data, size);
sample->cts = sample->dts = current_frame_index;
sample->index = videoSummaryIndex;
int result = lsmash_append_sample(root, videoTrackID, sample);
if (result != 0) {
    // エラー処理
}
```

### 動画の書き出し

全てのフレームを追加し終わったら `lsmash_flush_pooled_samples` を読んで全てのサンプルを書き出すように指示する。

`lsmash_finish_movie` を呼び出すとコンテナとして完全な状態で書き出される。

```c++
lsmash_flush_pooled_samples(root, videoTrackID, 0);
if (audioTrackID > 0) {
    lsmash_flush_pooled_samples(root, audioTrackID, 0);
}
int result = lsmash_finish_movie(root, NULL);
if (result != 0) {
    // エラー処理
}
```

### 終了処理

以下の関数を呼び出してクリーンアップする

- `lsmash_close_file`
- `lsmash_cleanup_summary`
- `lsmash_destroy_root`

`lsmash_cleanup_summary` は `lsmash_summary_t` にする必要があるのでダウンキャストする。

```c++
lsmash_close_file(&fileParameters);
lsmash_cleanup_summary(reinterpret_cast<lsmash_summary_t *>(audioSummary));
lsmash_cleanup_summary(reinterpret_cast<lsmash_summary_t *>(videoSummary));
lsmash_destroy_root(root);
```

### その他

#### LSMASH_CODEC_SPECIFIC_DATA_TYPE_QT_AUDIO_CHANNEL_LAYOUT の設定

```c++
{
    lsmash_codec_specific_t *spec = lsmash_create_codec_specific_data(LSMASH_CODEC_SPECIFIC_DATA_TYPE_QT_AUDIO_CHANNEL_LAYOUT, LSMASH_CODEC_SPECIFIC_FORMAT_STRUCTURED);
    lsmash_qt_audio_channel_layout_t *layout = static_cast<lsmash_qt_audio_channel_layout_t *>(spec->data.structured);
    layout->channelLayoutTag = QT_CHANNEL_LAYOUT_STEREO;
    lsmash_add_codec_specific_data(reinterpret_cast<lsmash_summary_t *>(audioSummary), spec);
    lsmash_destroy_codec_specific_data(spec);
}
```

#### エンコーダ名の設定

パスカル文字列である必要があるので、最初の1byte目に長さ、2byte目以降に実体を32byte以内に収める必要がある。

```c++
// 必須ではないがエンコーダを指定する場合は指定する
const char name[] = "some encoder";
videoSummary->compressorname[0] = static_cast<uint8_t>(sizeof(name) - 1);
memcpy(videoSummary->compressorname + 1, name, sizeof(videoSummary->compressorname) - 1);
```