🐨

Flutterで音声再生時に丸いアイコンが音声の抑揚とボリュームに合わせて動的に変わるようなUIを作成する方法

2024/10/19に公開

作りたいもの

ChatGPTモバイルアプリのVoiceモード時みたいに、再生している音声の抑揚とかボリュームに合わせてアニメーションが発生するUIを作りたいです。

https://www.youtube.com/watch?v=cjZdm30tbYA

調べてみたのですが、簡単に作れるライブラリは無さそうだったので自力で実装します。

実装

1. ベースの用意

まずはmain.dartファイルのbody内にAnimatedContainerWidgetを用意

main.dart
import 'dart:convert';
import 'dart:io';
import 'package:convert/convert.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() => runApp(
    const ProviderScope(child: MaterialApp(home: VoiceApp())));

class VoiceApp extends ConsumerStatefulWidget {
  const VoiceApp({super.key});

  
  _VoiceApp createState() => _VoiceApp();
}
class _VoiceApp extends ConsumerState<VoiceApp> {

  
  void initState() {
    super.initState();
  }

  
  void dispose() {
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: AnimatedContainer(
            duration: const Duration(milliseconds: 500),
            width: 50,
            height: 50,
            decoration: BoxDecoration(
              shape: BoxShape.circle,
              color: Colors.blue,
            ),
          ),
        ),
      ),
    );
  }
}


2. ControllerとSizeを変数化

次にAnimatedContainerのwidthとheightを変数で管理して動的に値が変えることができるようにします。
以下の二つの変数を定義します。

main.dart
  // ↓追加
  late AnimationController _animationController;
  late Animation<double> _animation;

  
  void initState() {
    super.initState();
   // ↓追加
    _animationController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 500),
    );
    _animation =
        Tween<double>(begin: 50.0, end: 100.0).animate(_animationController)
          ..addListener(() {
            setState(() {});
          });
  }

  
  void dispose() {
    // ↓追加
    _animationController.dispose();
    super.dispose();
  }


3. 変更させるサイズを決める関数を作成

次に音声データからサイズをどのくらい大きくするのか小さくするのかを計算する関数を作ります。
引数にはUint8List型に変換された音声データが渡ってくる想定です。

main.dart
 // ↓追加
 double calculateAmplitude(Uint8List chunk) {
    Int16List pcmData = Int16List.view(chunk.buffer);
    int maxAmplitude =
        pcmData.reduce((a, b) => a.abs() > b.abs() ? a : b).abs();
    int increaseAmount = 100;
    return maxAmplitude.toDouble() / 32768.0 * increaseAmount;
  }

4. 音声データをチャンク分けしてサイズを算出

次に音声データをチャンク分けして、先ほど作成した関数を実行させてサイズを算出します。
算出された値をAnmatedControllerに代入していくことで動的にサイズが変わっていきます。

音声データはファイルから読み取ったり、APIから音声データをストリーミング形式で受け取ったりと方法は様々ですが、以下の処理自体は変わることがないので、組み込めば大丈夫です。

main.dart
      ....

      await for (var chunk in response.stream) {
        double amplitude = calculateAmplitude(Uint8List.fromList(chunk));
        _animation = Tween<double>(begin: 50.0, end: 50.0 + amplitude)
            .animate(_animationController);
        _animationController.forward(from: 0.0);
        print(_animation.value);
      }

     ...


5. AnimatedContainerに変数を割り当てる

最後にAnimatedContainerのwidthとheightの値を変数に変えれば完成です。

main.dart
  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: AnimatedContainer(
            duration: const Duration(milliseconds: 500),
            width: _animation.value,
            height: _animation.value,
            decoration: BoxDecoration(
              shape: BoxShape.circle,
              color: Colors.blue,
            ),
          ),
        ),
      ),
    );
  }

こんな感じになる

ヘッドウォータース

Discussion