Zenn
Closed36

Flutter で record パッケージを使って録音/再生してみる

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

このスクラップについて

このスクラップでは record パッケージを使って音声を収録/再生する iOS アプリを作る過程を記録する。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

record パッケージについて

https://pub.dev/packages/record

  • マイクから入力される音声をファイルやストリームに出力できる
  • Android や iOS などマルチプラットフォームに対応している
  • iOS ではストリーム出力で利用可能なエンコーダは pcm16bits のみ

現時点のバージョンは 5.1.2 で Like は 636 件と多い。

flutter_sound というパッケージもあり、こちらの Like は 1,433 件とさらに多い。

Like だけ見ると flutter_sound の方が良さそうだが record の方が使い方がシンプルに見えるのでまずは record の方を使ってみようと思う。

record で不足する場合は flutter_sound を使ってみよう。

ライセンス的にも record が BSD-3-Clause であるのに対し、flutter_sound は MPL-2.0 なので制約が少ないのも良い。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

とりあえず iPhone で起動してみる

コマンド
open ios/Runner.xcworkspace


Bundle Identifer をユニークなものに変更する

VSCode で main.dart を開いてデバッグを開始する。

無事に起動したようだ。

スクリーンショットはどうやって撮影するんだっけ?

コマンド
flutter screenshot -o ~/Desktop/`date -Iseconds`.png


なぜかファイル名でコロンがスラッシュに変換される

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

アプリのひな形を作る

lib/main.dart
import 'package:flutter/material.dart';

void main() {
  runApp(const AudioRecordingApp());
}

class AudioRecordingApp extends StatelessWidget {
  const AudioRecordingApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Audio Recording Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const AudioRecordingPage(),
    );
  }
}

class AudioRecordingPage extends StatelessWidget {
  const AudioRecordingPage({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Audio recording demo app'),
      ),
      body: Padding(
        padding: EdgeInsets.all(16.0),
        child: Column(
          children: [
            SizedBox(
              width: double.infinity,
              child: ElevatedButton(
                onPressed: () {},
                child: const Text('Start recording'),
              ),
            ),
            const SizedBox(height: 8.0),
            SizedBox(
              width: double.infinity,
              child: ElevatedButton(
                onPressed: () {},
                child: const Text('Stop recording'),
              ),
            ),
            const SizedBox(height: 8.0),
            SizedBox(
              width: double.infinity,
              child: ElevatedButton(
                onPressed: () {},
                child: const Text('Play'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}


録音開始、録音終了、再生の 3 つのボタンを配置した

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

権限の設定

ios/Runner/Info.plist(追加分)
<dict>
    <key>NSMicrophoneUsageDescription</key>
    <string>Some message to describe why you need this permission</string>
</dict>
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

録音してみる

lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:record/record.dart';

void main() {
  runApp(const AudioRecordingApp());
}

class AudioRecordingApp extends StatelessWidget {
  const AudioRecordingApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Audio Recording Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const AudioRecordingPage(),
    );
  }
}

class AudioRecordingPage extends HookWidget {
  const AudioRecordingPage({super.key});

  
  Widget build(BuildContext context) {
    final audioRecorder = useMemoized(() => AudioRecorder());
    final isRecordingState = useState(false);
    final recordedAudioPathState = useState<String?>(null);

    final isStartButtonEnabled = !isRecordingState.value;
    final isStopButtonEnabled = isRecordingState.value;
    final isPlayButtonEnabled =
        !isRecordingState.value && recordedAudioPathState.value != null;

    onPressedStartButton() async {
      final directory = (await getApplicationDocumentsDirectory()).path;
      final filename = '${DateTime.now()}.bin';
      final destination = join(directory, filename);

      await audioRecorder.start(
        const RecordConfig(encoder: AudioEncoder.pcm16bits),
        path: destination,
      );

      isRecordingState.value = true;
    }

    onPressedStopButton() async {
      recordedAudioPathState.value = await audioRecorder.stop();
      isRecordingState.value = false;
    }

    onPressedPlayButton() {
      print(recordedAudioPathState.value);
    }

    return Scaffold(
      appBar: AppBar(
        title: const Text('Audio recording demo app'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            SizedBox(
              width: double.infinity,
              child: ElevatedButton(
                onPressed: isStartButtonEnabled ? onPressedStartButton : null,
                child: const Text('Start recording'),
              ),
            ),
            const SizedBox(height: 8.0),
            SizedBox(
              width: double.infinity,
              child: ElevatedButton(
                onPressed: isStopButtonEnabled ? onPressedStopButton : null,
                child: const Text('Stop recording'),
              ),
            ),
            const SizedBox(height: 8.0),
            SizedBox(
              width: double.infinity,
              child: ElevatedButton(
                onPressed: isPlayButtonEnabled ? onPressedPlayButton : null,
                child: const Text('Play'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Start recording ボタンを押してから Stop recording ボタンを押すと VSCode のデバッグコンソールに下記のようなメッセージが出力される。

flutter: /var/mobile/Containers/Data/Application/8AC122F9-F365-474F-929A-49DD6DACF30C/Documents/2024-10-09 18:39:18.426842

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

音声ファイルをちょっと覗いてみる

lib/main.dart(一部)
    onPressedPlayButton() async {
      final recordedAudioPath = recordedAudioPathState.value;

      if (recordedAudioPath == null) {
        return;
      }

      final recordedAudioFile = File(recordedAudioPath);
      final recordedAudioBytes = await recordedAudioFile.readAsBytes();

      print(recordedAudioBytes.length);

      for (var i = 0; i < 10; i += 1) {
        final bytes = [];
        for (var j = 0; j < 10; j += 1) {
          bytes.add(
            '0x${recordedAudioBytes[i * 10 + j].toRadixString(16).padLeft(2, '0')}',
          );
        }

        print(bytes.join(' '));
      }
    }
コンソール出力
flutter: 259932
flutter: 0x63 0x61 0x66 0x66 0x00 0x01 0x00 0x00 0x64 0x65
flutter: 0x73 0x63 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x20
flutter: 0x40 0xe5 0x88 0x80 0x00 0x00 0x00 0x00 0x6c 0x70
flutter: 0x63 0x6d 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x04
flutter: 0x00 0x00 0x00 0x01 0x00 0x00 0x00 0x02 0x00 0x00
flutter: 0x00 0x10 0x66 0x72 0x65 0x65 0x00 0x00 0x00 0x00
flutter: 0x00 0x00 0x0f 0xb0 0x00 0x00 0x00 0x00 0x00 0x00
3 flutter: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

長さ以外は毎回同じになるのでちゃんと録音できているか不安だ。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

WAVE 形式で保存する

lib/main.dart(一部)
    onPressedStartButton() async {
      final directory = (await getApplicationDocumentsDirectory()).path;
      final filename = '${DateTime.now()}.wav';
      final destination = join(directory, filename);

      await audioRecorder.start(
        const RecordConfig(encoder: AudioEncoder.wav),
        path: destination,
      );

      isRecordingState.value = true;
    }
コンソール出力
flutter: 199720
flutter: 0x52 0x49 0x46 0x46 0x20 0x0c 0x03 0x00 0x57 0x41
flutter: 0x56 0x45 0x4a 0x55 0x4e 0x4b 0x1c 0x00 0x00 0x00
2 flutter: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
flutter: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x66 0x6d
flutter: 0x74 0x20 0x10 0x00 0x00 0x00 0x01 0x00 0x02 0x00
flutter: 0x44 0xac 0x00 0x00 0x10 0xb1 0x02 0x00 0x04 0x00
flutter: 0x10 0x00 0x46 0x4c 0x4c 0x52 0xa8 0x0f 0x00 0x00
2
flutter: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

バイナリを読むと 2 行目に WAVEJUNK という文字列があるようだがどういう意味なのだろう?

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

JUNK チャンクについて

この辺が参考になりそう。

https://www.iridoatelier.net/sb/log/eid283.html

https://www.wdic.org/w/TECH/WAV

どうやら色々な都合で無効になってしまったデータの範囲のようだ。

よく見たら 0x66 0x6d 0x74 0x20 は fmt でここからフォーマット情報が始まっているようだ。

0x10 ということは 16 バイトなのか?

  • フォーマットは 0x01 なのでリニアPCM
  • チャンネル数は 0x02 なのでステレオ
  • サンプリング周波数は 0xac44 なので 44,100 Hz
  • データ速度は 0x02b110 なので 176,400 バイト(44,100 Hz × 2 Channel × 16 bit)
  • ブロックサイズは 0x04 なので 4
  • ビットレートは 0x10 なので 16 bit / sample

その後の FLLR チャンクとは何なのだろうか?

https://stackoverflow.com/questions/6284651/avaudiorecorder-doesnt-write-out-proper-wav-file-header

どうやら Apple 製品の WAVE ファイルで fmt と data の間に挟まれるようだ。

そのサイズは 0x0fa8 と 4,008 バイトもあるのでなるほどこれでは音声が出てこない訳だ。

明日は FLLR チャンクが終わった後のバイナリを見てみよう。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

バイト数を数える

  • RIFF
  • サイズ
  • WAVE
  • JUNK
    • サイズ
    • データ(28 バイト)
  • fmt
    • サイズ
    • データ(16 バイト)
  • FLLR
    • サイズ
    • データ(4,008 バイト)

合計で 4,088 バイトなので 4,089 バイト目から次のちゃくが始まるようだ。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

次のチャンクを見てみる

lib/main.dart(一部)
          bytes.add(
            '0x${recordedAudioBytes[4088 + i * 10 + j].toRadixString(16).padLeft(2, '0')}',
          );
コンソール出力
flutter: 222300
flutter: 0x64 0x61 0x74 0x61 0x5c 0x54 0x03 0x00 0x00 0x00
7 flutter: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
flutter: 0xff 0xff 0xff 0xff 0x01 0x00 0x01 0x00 0xff 0xff
flutter: 0xff 0xff 0x01 0x00 0x01 0x00 0xfd 0xff 0xfd 0xff

ついに data チャンクが登場した、今回のデータサイズは 0x03545c なので 218,204 バイトのようだ。

4,088 + 4 + 4 + 218,204 = 222,300 バイトなので data チャンクで最後のようだ。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

WAVE ファイルに関してわかったこと

iPhone で record パッケージを使って WAV 形式で録音すると次の 4 つのチャンクから構成される音声ファイルが作成されるようだ。

  • JUNK: 28 バイト
  • fmt : 16 バイト
  • FLLR: 4,088 バイト
  • data: 録音時間によって変動
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

今更だけど AudioRecorder の dispose

lib/main.dart(一部)
    final audioRecorder = useMemoized(() => AudioRecorder());

    useEffect(() {
      return () => audioRecorder.dispose();
    }, []);
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

AudioPlayer のセットアップ

lib/main.dart(一部)
    final audioPlayer = useMemoized(() {
      final audioPlayer = AudioPlayer();
      audioPlayer.setReleaseMode(ReleaseMode.stop);
      return audioPlayer;
    });

    useEffect(() {
      return () => audioPlayer.dispose();
    }, []);
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Play ボタンのイベントハンドラ変更

lib/main.dart(一部)
    onPressedPlayButton() async {
      final recordedAudioPath = recordedAudioPathState.value;

      if (recordedAudioPath == null) {
        return;
      }

      await audioPlayer.play(DeviceFileSource(recordedAudioPath));
    }
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

現状の AudioRecordingPage

lib/main.dart(一部)
class AudioRecordingPage extends HookWidget {
  const AudioRecordingPage({super.key});

  
  Widget build(BuildContext context) {
    final audioRecorder = useMemoized(() => AudioRecorder());

    useEffect(() {
      return () => audioRecorder.dispose();
    }, []);

    final audioPlayer = useMemoized(() {
      final audioPlayer = AudioPlayer();
      audioPlayer.setReleaseMode(ReleaseMode.stop);
      return audioPlayer;
    });

    useEffect(() {
      return () => audioPlayer.dispose();
    }, []);

    final isRecordingState = useState(false);
    final recordedAudioPathState = useState<String?>(null);

    final isStartButtonEnabled = !isRecordingState.value;
    final isStopButtonEnabled = isRecordingState.value;
    final isPlayButtonEnabled =
        !isRecordingState.value && recordedAudioPathState.value != null;

    onPressedStartButton() async {
      final directory = (await getApplicationDocumentsDirectory()).path;
      final filename = '${DateTime.now()}.wav';
      final destination = join(directory, filename);

      await audioRecorder.start(
        const RecordConfig(encoder: AudioEncoder.wav),
        path: destination,
      );

      isRecordingState.value = true;
    }

    onPressedStopButton() async {
      recordedAudioPathState.value = await audioRecorder.stop();
      isRecordingState.value = false;
    }

    onPressedPlayButton() async {
      final recordedAudioPath = recordedAudioPathState.value;

      if (recordedAudioPath == null) {
        return;
      }

      await audioPlayer.play(DeviceFileSource(recordedAudioPath));
    }

    return Scaffold(
      appBar: AppBar(
        title: const Text('Audio recording demo app'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            SizedBox(
              width: double.infinity,
              child: ElevatedButton(
                onPressed: isStartButtonEnabled ? onPressedStartButton : null,
                child: const Text('Start recording'),
              ),
            ),
            const SizedBox(height: 8.0),
            SizedBox(
              width: double.infinity,
              child: ElevatedButton(
                onPressed: isStopButtonEnabled ? onPressedStopButton : null,
                child: const Text('Stop recording'),
              ),
            ),
            const SizedBox(height: 8.0),
            SizedBox(
              width: double.infinity,
              child: ElevatedButton(
                onPressed: isPlayButtonEnabled ? onPressedPlayButton : null,
                child: const Text('Play'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

だいぶ日が空いてしまった

まずは config を pcm16bits に変更して再生できるかどうか試してみよう。

lib/main.dart(一部)
    onPressedStartButton() async {
      final directory = (await getApplicationDocumentsDirectory()).path;
      final filename = '${DateTime.now()}.wav';
      final destination = join(directory, filename);

      await audioRecorder.start(
        const RecordConfig(encoder: AudioEncoder.pcm16bits),
        path: destination,
      );

      isRecordingState.value = true;
    }

普通に再生できる、もしかして WAV 形式なのだろうか?

中身を見てみよう。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

再びバイナリ分析

lib/main.dart(一部)
    onPressedPlayButton() async {
      final recordedAudioPath = recordedAudioPathState.value;

      if (recordedAudioPath == null) {
        return;
      }

      final recordedAudioBytes = await File(recordedAudioPath).readAsBytes();

      for (var i = 0; i < 10; i += 1) {
        final bytes = [];
        for (var j = 0; j < 10; j += 1) {
          bytes.add(
            '0x${recordedAudioBytes[i * 10 + j].toRadixString(16).padLeft(2, '0')}',
          );
        }

        print(bytes.join(' '));
      }

      await audioPlayer.play(DeviceFileSource(recordedAudioPath));
    }
コンソール出力
flutter: 0x52 0x49 0x46 0x46 0xf0 0xff 0x04 0x00 0x57 0x41
flutter: 0x56 0x45 0x4a 0x55 0x4e 0x4b 0x1c 0x00 0x00 0x00
2
flutter: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
flutter: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x66 0x6d
flutter: 0x74 0x20 0x10 0x00 0x00 0x00 0x01 0x00 0x02 0x00
flutter: 0x44 0xac 0x00 0x00 0x10 0xb1 0x02 0x00 0x04 0x00
flutter: 0x10 0x00 0x46 0x4c 0x4c 0x52 0xa8 0x0f 0x00 0x00
2
flutter: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

前は caff チャンクが出力されたような気がするが。。。

一旦アプリを再起動してみよう。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

原因がわかった

拡張子が .wav だと WAVE 形式になるようだ。

lib/main.dart(一部)
    onPressedStartButton() async {
      final directory = (await getApplicationDocumentsDirectory()).path;
      final filename = '${DateTime.now()}';
      final destination = join(directory, filename);

      await audioRecorder.start(
        const RecordConfig(encoder: AudioEncoder.wav),
        path: destination,
      );

      isRecordingState.value = true;
    }

.bin などでも良いかもしれない。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

ようやく再現できた

コンソール出力
flutter: 0x63 0x61 0x66 0x66 0x00 0x01 0x00 0x00 0x64 0x65
flutter: 0x73 0x63 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x20
flutter: 0x40 0xe5 0x88 0x80 0x00 0x00 0x00 0x00 0x6c 0x70
flutter: 0x63 0x6d 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x04
flutter: 0x00 0x00 0x00 0x01 0x00 0x00 0x00 0x02 0x00 0x00
flutter: 0x00 0x10 0x66 0x72 0x65 0x65 0x00 0x00 0x00 0x00
flutter: 0x00 0x00 0x0f 0xb0 0x00 0x00 0x00 0x00 0x00 0x00
3
flutter: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

バイナリ解読

  • はじめの caff は CAF ファイルであることを示す
  • 次の 2 バイトはバージョン
  • 次の 2 バイトはフラグ
  • その後は desc チャンクであり、長さは 8 バイトで示す、この場合は 0x20 なので 32 バイトとなる
struct CAFAudioFormat {
    Float64 mSampleRate;
    UInt32  mFormatID;
    UInt32  mFormatFlags;
    UInt32  mBytesPerPacket;
    UInt32  mFramesPerPacket;
    UInt32  mChannelsPerFrame;
    UInt32  mBitsPerChannel;
};
  • 8 + 4 × 6 = 32 バイトなので計算は合っている
  • ちなみにネットワークバイトオーダー(ビッグエンディアン)のようだ
  • mSampleRate の 0x40 0xe5 0x88 0x80 0x00 0x00 0x00 0x00 は Float64 で 44100 なのでこれは正しい
  • mFormatId の 0x6c 0x70 0x63 0x6d は lpcm で Linear PCM を示すようだ
  • mFormatFloags の 0x00 0x00 0x00 0x02 は kCAFLinearPCMFormatFlagIsLittleEndian でデータがリトルエンディアンであることを示すようだ
  • mBytesPerPacket の 0x00 0x00 0x00 0x04 はパケットという単位がよくわからないが 4 バイトのようだ
  • mFramesPerPacket の 0x00 0x00 0x00 0x01 はフレームという単位もよくからないが 1 フレームのようだ
  • mChannelsPerFrame の 0x00 0x00 0x00 0x02 は 1 フレームには 2 チャンネルが含まれるようだ
  • mBitsPerChannel の 0x00 0x00 0x00 0x10 は 1 チャンネルには 16 ビットが含まれるようだ
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Free チャンク

フォーマットの情報の後は free チャンクが始まる。

The optional Free chunk is for reserving space, or providing padding, in a CAF file. The contents of the Free chunk data section have no significance and should be ignored.

無視しても良さそうだ。

ちなみにサイズは 0x00 0x00 0x00 0x00 0x00 0x00 0x0f 0xb0 = 4016 バイトなのでかなりスキップすることになる。

  • caff + バージョン + フラグ:8 バイト
  • desc + サイズ + データ:4 + 8 + 32 = 44 バイト
  • free + サイズ + データ:4 + 8 + 4016 = 4,028 バイト

合計で 4,080 バイトをスキップすれば次のチャンクが見れそうだ。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

次は data チャンクだった

コンソール出力
flutter: 0x64 0x61 0x74 0x61 0x00 0x00 0x00 0x00 0x00 0x02
flutter: 0x02 0x48 0x00 0x00 0x00 0x01 0x00 0x00 0x00 0x00
8 flutter: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

data チャンクとはいえども WAVE 形式とは若干異なり最初の 4 バイトが編集回数になっている。

struct CAFData {
    UInt32 mEditCount;                 // initially set to 0
    UInt8 mData [kVariableLengthArray];
};

また、サイズが 8 バイトのビッグエンディアンなのでそこも注意した方が良さそうだ。

ちなみに data チャンクの後は何かあるのだろうか?

4 + 8 + 0x020248 = 131,668 で 4080 と足すと 135,748 バイトとなる。

print(recordedAudioBytes.length); を追加してサイズを確認したら 135,748 バイトだったので、どうやら data チャンクが最後のようだ。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

CAF として保存したら再生できる?

lib/main.dart(一部)
    onPressedStartButton() async {
      final directory = (await getApplicationDocumentsDirectory()).path;
      final filename = '${DateTime.now()}.caf';
      final destination = join(directory, filename);

      await audioRecorder.start(
        const RecordConfig(encoder: AudioEncoder.wav),
        path: destination,
      );

      isRecordingState.value = true;
    }

無事に再生できた。

ストリーミングの場合は data チャンクを繋ぎ合わせてチャンクサイズを調整すれば再生できるようになるのだろうか?

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

試行錯誤の上、ようやくデータ取得

lib/main.dart(一部)
    final subscriptionRef = useRef<StreamSubscription<Uint8List>?>(null);
    final audioChunksRef = useRef(<Uint8List>[]);

    void onAudioDataAvailable(Uint8List audioChunk) {
      audioChunksRef.value = [...audioChunksRef.value, audioChunk];
    }

    Future<void> onPressedStartButton() async {
      // final directory = (await getApplicationDocumentsDirectory()).path;
      // final filename = '${DateTime.now()}.caf';
      // final destination = join(directory, filename);

      // await audioRecorder.start(
      //   const RecordConfig(encoder: AudioEncoder.pcm16bits),
      //   path: destination,
      // );

      audioChunksRef.value = [];

      final stream = await audioRecorder
          .startStream(const RecordConfig(encoder: AudioEncoder.pcm16bits));
      subscriptionRef.value = stream.listen(onAudioDataAvailable);

      isRecordingState.value = true;
    }

    Future<void> onPressedStopButton() async {
      // recordedAudioPathState.value = await audioRecorder.stop();

      subscriptionRef.value?.cancel();

      final recordedAudioBytes = audioChunksRef.value[0];

      print(recordedAudioBytes.length);

      for (var i = 0; i < 10; i += 1) {
        final bytes = [];
        for (var j = 0; j < 10; j += 1) {
          bytes.add(
            '0x${recordedAudioBytes[0 + i * 10 + j].toRadixString(16).padLeft(2, '0')}',
          );
        }

        print(bytes.join(' '));
      }

      isRecordingState.value = false;
    }
コンソール出力
flutter: 17640
8
flutter: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
flutter: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0xff 0xff
flutter: 0xff 0xff 0xfd 0xff 0xfd 0xff 0x0e 0x00 0x0e 0x00

何というか生データ感がある。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

意外とスムーズにできた

lib/main.dart(一部)
    Future<void> onPressedStopButton() async {
      // recordedAudioPathState.value = await audioRecorder.stop();

      subscriptionRef.value?.cancel();

      final audioChunks = audioChunksRef.value;

      const chunkNameBytes = 4;
      const chunkSizeBytes = 4;
      const chunkHeaderBytes = chunkNameBytes + chunkSizeBytes;
      const fmtChunkBodyBytes = 16;
      const initialBytes = 12;
      const fmtChunkBytes = chunkHeaderBytes + fmtChunkBodyBytes;

      final dataChunkBytes = chunkHeaderBytes +
          audioChunks
              .map((audioChunk) => audioChunk.length)
              .reduce((v, e) => v + e);

      final totalBytes = initialBytes + fmtChunkBytes + dataChunkBytes;
      final byteData = ByteData(
          initialBytes + fmtChunkBytes + chunkHeaderBytes + fmtChunkBodyBytes);

      byteData.setUint8(0, 'R'.codeUnitAt(0));
      byteData.setUint8(1, 'I'.codeUnitAt(0));
      byteData.setUint8(2, 'F'.codeUnitAt(0));
      byteData.setUint8(3, 'F'.codeUnitAt(0));
      byteData.setUint32(4, totalBytes - 8, Endian.little);
      byteData.setUint8(8, 'W'.codeUnitAt(0));
      byteData.setUint8(9, 'A'.codeUnitAt(0));
      byteData.setUint8(10, 'V'.codeUnitAt(0));
      byteData.setUint8(11, 'E'.codeUnitAt(0));
      byteData.setUint8(12, 'f'.codeUnitAt(0));
      byteData.setUint8(13, 'm'.codeUnitAt(0));
      byteData.setUint8(14, 't'.codeUnitAt(0));
      byteData.setUint8(15, ' '.codeUnitAt(0));
      byteData.setUint32(16, fmtChunkBodyBytes, Endian.little);

      const linearPcmFormatId = 0x01;
      const numOfChannel = 2;
      const samplingFrequency = 44100;
      const bitsPerSample = 16;
      const bytesPerSample = bitsPerSample ~/ 8;
      const bytesPerSecond = samplingFrequency * bytesPerSample * numOfChannel;
      const blockSize = bytesPerSample * numOfChannel;

      byteData.setUint16(20, linearPcmFormatId, Endian.little);
      byteData.setUint16(22, numOfChannel, Endian.little);
      byteData.setUint32(24, samplingFrequency, Endian.little);
      byteData.setUint32(28, bytesPerSecond, Endian.little);
      byteData.setUint16(32, blockSize, Endian.little);
      byteData.setUint16(34, bitsPerSample, Endian.little);
      byteData.setUint8(36, 'd'.codeUnitAt(0));
      byteData.setUint8(37, 'a'.codeUnitAt(0));
      byteData.setUint8(38, 't'.codeUnitAt(0));
      byteData.setUint8(39, 'a'.codeUnitAt(0));
      byteData.setUint32(40, dataChunkBytes, Endian.little);

      final directory = (await getApplicationDocumentsDirectory()).path;
      final filename = '${DateTime.now()}.wav';
      final destinationPath = join(directory, filename);
      final destinationFile = File(destinationPath);

      await destinationFile.writeAsBytes(byteData.buffer.asUint8List());

      for (final audioChunk in audioChunks) {
        await destinationFile.writeAsBytes(audioChunk, mode: FileMode.append);
      }

      recordedAudioPathState.value = destinationPath;
      isRecordingState.value = false;
    }
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

これで検証したいことは大体終わった

まだストリームの止め方などはわかっていないが自分の知りたい事はわかったのでこれでスクラップをクローズしよう。

お疲れさまでした自分!

このスクラップは5ヶ月前にクローズされました
ログインするとコメントできます