[JavaScript] ArrayBufferについて調べてみた


ArrayBuffer、DataView、TypedArrayはJSからバイナリデータを扱うための仕様です。
どういう操作があるのかを改めて確認してみました。

まずは簡単に各クラスの定義から整理。

## ArrayBuffer

物理メモリの領域（バッファ）を確保するためのクラス。
ただし、確保するのみでバッファに対する操作は提供していません。
バッファの操作は`TypedArray`か`DataView`いずれかのクラスを利用します。

### ArrayBufferのプロパティ

- byteLength ... バッファの総バイトサイズ。

### ArrayBufferのメソッド

- slice() ... バッファの開始位置と終了位置を指定して、新しいArrayBufferを生成します。


## TypedArray

いわゆる型付き配列。`Float32Array`や`Uint8Array`など、型を指定してバッファから配列を生成する。
通常の配列（Array）のように添字によってバッファにアクセス可能で、「高速に」読み書きが行えます。

ただ、配列構造を自身で把握し適切に処理を行わないとエラーが出るなど低レベルな機能のみを提供しています。
[（アライメント（バイト境界）](http://www.ertl.jp/~takayuki/readings/info/no01.html)や[エンディアン方式](http://ja.wikipedia.org/wiki/%E3%82%A8%E3%83%B3%E3%83%87%E3%82%A3%E3%82%A2%E3%83%B3)の違いなどを自身で考慮する必要がある）

#### アライメント（バイト境界）

アライメントというのがあり、バイト数を適切に設定しないとエラーとなってしまいます。
具体的には生成するビューのビット数に応じて整数倍となるバイト数を設定しないとなりません。
例えば、`Uint16Array`を使ってビューを生成する場合、16bit = 2byte、つまり2の整数倍のバイト数を指定しないとエラーとなります。
（Float64Arrayの場合は64bit = 8byteなので、8の整数倍のバイト数が必要）

中途半端なバイト数で生成してしまうと、確保したメモリ領域を超えてしまう可能性があるためエラーとなるのでしょう。
このあたりはメモリをダイレクトに使わないJSではイメージしづらいかもしれません。


#### エンディアン方式

マルチバイトサイズとなる型のクラスを利用する場合、特に実行環境が読めないJSではしっかりと考慮する必要があります。

## DataView

TypedArrayよりも高機能なバッファ操作用クラス。
`getUint32`などのように分かりやすいメソッドが用意されています。

### DataViewのプロパティ

- buffer ... 参照しているArrayBuffer
- byteOffset ... ビューの開始位置（バイト数）を取得
- byteLength ... ビューからアクセス可能なサイズ（バイト数）を取得

### DataViewのメソッド

#### Getter

| メソッド | 意味 |
|:---------|:--------------|
| getInt8 | バッファから 8bit整数値（符号あり）を読み込む |
| getUint8 | バッファから 8bit整数値（符号なし）を読み込む |
| getInt16 | バッファから 16bit整数値（符号あり）を読み込む |
| getUint16 | バッファから 16bit整数値（符号なし）を読み込む |
| getInt32 | バッファから 32bit整数値（符号あり）を読み込む |
| getUint32 | バッファから 32bit整数値（符号なし）を読み込む |
| getFloat32 | バッファから 32bit浮動小数点数を読み込む |
| getFloat64 | バッファから 64bit浮動小数点数を読み込む |

#### Setter

| メソッド | 意味 |
|:---------|:--------------|
| setInt8 | バッファへ 8bit整数値（符号あり）を書き込む |
| setUint8 | バッファへ 8bit整数値（符号なし）を書き込む |
| setInt16 | バッファへ 16bit整数値（符号あり）を書き込む |
| setUint16 | バッファへ 16bit整数値（符号なし）を書き込む |
| setInt32 | バッファへ 32bit整数値（符号あり）を書き込む |
| setUint32 | バッファへ 32bit整数値（符号なし）を書き込む |
| setFloat32 | バッファへ 32bit浮動小数点数を書き込む |
| setFloat64 | バッファへ 64bit浮動小数点数を書き込む |


## バイナリデータを扱う

### TypedArrayから扱う

ビューを使う場合、バイナリデータから特定のバイト列を取り出したり、書き込んだりすることが主な目的となると思います。

例えば、4byte + 2byte + 4byteのデータ構造でそれぞれにデータを書き込みたいとします。
その場合は以下のようにバッファの生成と書き込みを行います。

```js:create-buffer
var buffer = new ArrayBuffer(4 + 2 + 4); // 4 + 2 + 4byteのバッファを確保

// 4byteの書き込みを行うビューを生成
var ary_u16 = new Uint16Array(buffer, 0, 2);
ary_u16[0] = 0x0001;
ary_u16[1] = 0x0100;

// 1byteの書き込みを行うビューを生成
var ary_u8 = new Uint8Array(buffer, 4, 2);
ary_u8[0] = 0xff;
ary_u8[1] = 0x0f;

// 4byteの書き込みを行うビューを生成
var ary_i16 = new Int16Array(buffer, 6, 2);
ary_i16[0] = -50;
ary_i16[1] = -30;

var result = new Uint8Array(buffer);

console.log(result); // => [1, 0, 0, 1, 255, 15, 206, 255, 226, 255]
```


### DataViewから扱う

バイナリに意図したデータを書き込んだり、色々な種類のデータをパックするのに使われるのがこちらのDataViewです。

上記と同じ処理を、DataViewを使って行うと以下のようになります。

```js:create-buffer-dataview
var buffer = new ArrayBuffer(4 + 2 + 4);
var view = new DataView(buffer);

var offset = 0;

// 4byteの書き込み
view.setUint16(offset, 0x0001, true);
offset += 2;
view.setUint16(offset, 0x0100, true);
offset += 2;

// 2byteの書き込み
view.setUint8(offset, 0xff);
offset += 1;
view.setUint8(offset, 0x0f);
offset += 1;

// 4byteの書き込み
view.setInt16(offset, -50, true);
offset += 2;
view.setUint16(offset, -30, true);
offset += 2;

var result = new Uint8Array(buffer);

console.log(result); // =>  [1, 0, 0, 1, 255, 15, 206, 255, 226, 255]
```

## CanvasのImagaDataはTypedArray

Canvasの`getImagaData`で取得される`ImageData`オブジェクトの`data`プロパティは`Uint8ClampedArray`になっています。
これもTypedArrayで、直に操作することで画像をバイナリデータとして扱うことができます。

こちらの配列は画素なのでイメージしやすく、実際に操作したことがある人も少なくないのではないでしょうか。
ともあれ、バイナリデータを扱えると色々と活用できそうなのでしっかりと把握しておきたいですね。