kintoneで録音してWAVファイルを保存する

kintoneで録音したい！
というわけで

下記サイトを参考に作ってみました。

* [cybozu developer network ナレッジノート-MediaRecorder APIを使って録音アプリをつくろう](https://developer.cybozu.io/hc/ja/community/posts/360041597711-MediaRecorder-API%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E9%8C%B2%E9%9F%B3%E3%82%A2%E3%83%97%E3%83%AA%E3%82%92%E3%81%A4%E3%81%8F%E3%82%8D%E3%81%86)
* [ブラウザで録音してwavで保存](https://qiita.com/optimisuke/items/f1434d4a46afd667adc6)
* [音ファイル（拡張子：WAVファイル）のデータ構造について](https://www.youfit.co.jp/archives/1418)
* [MediaRecorder - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/MediaRecorder)

出来上がったもの（音量注意）

<iframe width="480" height="360" src="https://www.youtube.com/embed/3QBRHJA38pU" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>


# アプリの準備

フィールドは一つだけ。

| フィールド種類 | フィールドコード |  
|:-:|:-:|:-:|
| 添付ファイル  | 音声  |   

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536148/42b25b60-90d0-1ed8-85ad-694dc22b3918.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536148/c4fd079a-9dce-68de-760b-f1465004b4d9.png)

# JavaScript

ライブラリの準備お忘れなく

kintone REST API Client 
`https://unpkg.com/@kintone/rest-api-client@2.0.14/umd/KintoneRestAPIClient.min.js`

kintone UI Component
`https://unpkg.com/kintone-ui-component/umd/kuc.min.js`

Luxon
`https://js.cybozu.com/luxon/2.0.1/luxon.min.js`

また、音声ファイルにプレイヤーをつける方法はコチラを参考にどうぞ。

https://qiita.com/juri_don/items/243694d5b1b6cdd08d8b

## 一覧表示後イベントでボタン設置

録音開始ボタンと録音停止ボタンを設置します。

```js
// 一覧画面のメニュー下にボタン設置する
const sp = kintone.app.getHeaderSpaceElement();
const fname = new Kuc.Text({
  placeholder: "音声ファイル名",
  suffix: ".wav",
  textAlign: "left",
  visible: true,
  disabled: false,
});
const startButton = new Kuc.Button({
  text: "🔴録音開始",
  type: "submit",
});
const stopButton = new Kuc.Button({
  text: "⬛録音停止",
  type: "submit",
  disabled: true,
});

sp?.appendChild(fname);
sp?.appendChild(startButton);
sp?.appendChild(stopButton);
```

## 参考サイトのソースをコピーしたものを貼り付けていく

ボタン設置の次の行から
冒頭で紹介した参考サイト（[ブラウザで録音してwavで保存](https://qiita.com/optimisuke/items/f1434d4a46afd667adc6)）
からコピーしてきてESLintに怒られたところを直したりしたコードを貼り付けます。

```js
// 録音に必要な変数など宣言
let audio_sample_rate = null;
let scriptProcessor = null;
let audioContext = null;
const audioData = [];
const bufferSize = 1024;

// 録音データを保存する
const saveAudio = async () => {
  // 録音データを加工する
  // kintoneに保存する
}
```

## 録音データを加工する
冒頭で紹介した[こちらのページ](https://www.youfit.co.jp/archives/1418)を参考に録音した音声データをファイルにします。

```js
// 録音データを加工する
// viewに文字を1文字ずつ書き込む
const writeString = (v, offset, string) => {
  for (let i = 0; i < string.length; i++) {
    v.setUint8(offset + i, string.charCodeAt(i));
  }
};

// float(32bit)を16bitに変換する
const floatTo16BitPCM = (output, offset, input) => {
  for (let i = 0; i < input.length; i++, offset += 2) {
    const s = Math.max(-1, Math.min(1, input[i]));
    output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true);
  }
};

// 録音終了
await audioContext.close();

// audioDataを1次元配列にする。
const sampleLength = audioData.reduce((a, c) => a + c.length, 0);
const samples = new Float32Array(sampleLength);
let sampleIdx = 0;
for (let i = 0; i < audioData.length; i++) {
  for (let j = 0; j < audioData[i].length; j++) {
    samples[sampleIdx] = audioData[i][j];
    sampleIdx++;
  }
}

// PCM 16bit なので、1要素あたり 16bit = 2Byte つまり要素数*2でwavのチャンクサイズを計算する
const wavsize = samples.length * 2;
const buffer = new ArrayBuffer(44 + wavsize);
const view = new DataView(buffer);

writeString(view, 0, "RIFF"); // RIFF識別子 RIFFヘッダ 4Byte
view.setUint32(4, 32 + wavsize, true); // チャンクサイズ （44Byte -8 ）+ データサイズ 4Byte
writeString(view, 8, "WAVE"); // フォーマット WAVEヘッダ 4Byte
writeString(view, 12, "fmt "); // fmt識別子 4Byte
view.setUint32(16, 16, true); // fmtチャンクのバイト数 4Byte
view.setUint16(20, 1, true); // 音声フォーマットID 2Byte
view.setUint16(22, 1, true); // チャンネル数 2Byte
view.setUint32(24, audio_sample_rate, true); // サンプリング周波数 4Byte
view.setUint32(28, audio_sample_rate * 2, true); // 1秒あたりバイト数の平均 データ速度 4Byte
view.setUint16(32, 2, true); // ブロックサイズ 2Byte
view.setUint16(34, 16, true); // ビット／サンプル サンプルあたりのビット数 2Byte
writeString(view, 36, "data"); // サブチャンク識別子 data 4Byte
view.setUint32(40, wavsize, true); // サブチャンクサイズ 波形データのバイト数 4Byte
floatTo16BitPCM(view, 44, samples); // 波形データ
```


## kintoneに音声ファイルを保存
kintone REST API Clientのファイル保存メソッドを使って、
作成した音声データをアップロードします。

```js
// kintoneに保存する
const client = new KintoneRestAPIClient();
const date = luxon.DateTime.local();
const FILE = {
  name: fname.value
    ? fname.value + "_" + date.toFormat("yyyyMMddHHmmss") + ".wav"
    : "音声_" + date.toFormat("yyyyMMddHHmmss") + ".wav",
  data: new Blob([view], { type: "audio/wav" }),
};
const { fileKey } = await client.file.uploadFile({
  file: FILE,
});
const { id } = await client.record.addRecord({
  app: kintone.app.getId(),
  record: {
    音声: {
      value: [{ fileKey }],
    },
  },
});

// 保存処理が終わったらリロード
location.reload();
```

## 録音開始ボタンと録音停止ボタンの処理

```js
// 録音開始ボタン
startButton.addEventListener("click", function () {
  startButton.text = "💖録音中💖";
  stopButton.disabled = false;
  // getUserMedia
  const stream = navigator.mediaDevices
    .getUserMedia({ audio: true, video: false })
    .then(handleSuccess);
});

// 録音停止ボタン
stopButton.addEventListener("click", function () {
  saveAudio();
});
```

## 参考サイトのコード残りを貼り付ける

再び・・・冒頭で紹介した参考サイト（[ブラウザで録音してwavで保存](https://qiita.com/optimisuke/items/f1434d4a46afd667adc6)）
からコピーしてきてESLintに怒られたところを直していったコードを貼り付けます。
[MediaRecorder - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/MediaRecorder)をご参照ください。

```js
// save audio data
const onAudioProcess = (e) => {
  const input = e.inputBuffer.getChannelData(0);
  const bufferData = new Float32Array(bufferSize);
  for (let i = 0; i < bufferSize; i++) {
    bufferData[i] = input[i];
  }
  audioData.push(bufferData);
};

// getusermedia
const handleSuccess = (stream) => {
  audioContext = new AudioContext();
  audio_sample_rate = audioContext.sampleRate;
  scriptProcessor = audioContext.createScriptProcessor(bufferSize, 1, 1);
  const mediastreamsource = audioContext.createMediaStreamSource(stream);
  mediastreamsource.connect(scriptProcessor);
  scriptProcessor.onaudioprocess = onAudioProcess;
  scriptProcessor.connect(audioContext.destination);
};
```

## 全体的なコードはコチラ

https://gist.github.com/juridon/ecbe16c2a468b53f4b0f1ed769498969

お好みで保存時にファイル名を変えてみたり、コメントを追加してみたり、色々できそうな気がします。

# まとめ
楽器の練習を録音していちいちkintoneに保存するのが面倒だったので作ってみました。
録音の重要な部分は参考サイトのコードをほぼほぼコピーして作りましたが
なんとかこれで、自分の演奏を客観的に見る（？）ことができるゾ！