Flutter for Harmony 跨平台开发实战:鸿蒙与音乐律动艺术、FFT 频谱能量场:正弦函数的叠加艺术

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

一、引言:从声波到视觉的跨域映射

在数字音频的世界里,声音是一串随时间变化的采样数据。而当我们想要"看见"声音时,就需要将这些时域信号转换为频域信号------这正是 FFT(快速傅里叶变换) 的魔力所在。

本篇文章将带你深入探索:

  • 🎵 FFT 原理与实现:从数学公式到代码实践
  • 🌊 正弦波叠加艺术:多频率波形的视觉呈现
  • 📊 频谱能量场构建:动态柱状图与波形可视化
  • 🎨 OpenHarmony 适配:跨平台音频可视化方案

二、FFT 原理:揭开频谱分析的神秘面纱

📚 2.1 傅里叶变换的数学基础

傅里叶变换的核心思想是:任何周期函数都可以表示为不同频率正弦波的叠加

数学表达式:

复制代码
X(k) = Σ x(n) * e^(-j*2π*k*n/N)

其中:

  • x(n) 是时域采样信号
  • X(k) 是频域变换结果
  • N 是采样点数
  • k 是频率索引

🎯 2.2 FFT 算法优化

传统 DFT 的时间复杂度为 O(N²),而 FFT 利用对称性和周期性将其优化到 O(N*logN)。

dart 复制代码
import 'dart:math';
import 'dart:typed_data';

class FFTProcessor {
  final int size;
  late final Float32List _real;
  late final Float32List _imag;
  
  FFTProcessor(this.size) {
    _real = Float32List(size);
    _imag = Float32List(size);
  }
  
  void compute(Float32List input) {
    final n = input.length;
  
    // 初始化实部和虚部
    for (int i = 0; i < n; i++) {
      _real[i] = input[i];
      _imag[i] = 0;
    }
  
    // 位反转排列
    _bitReverse(n);
  
    // FFT 蝶形运算
    for (int s = 1; s <= _log2(n); s++) {
      final m = 1 << s;
      final m2 = m >> 1;
      final wmReal = cos(2 * pi / m);
      final wmImag = -sin(2 * pi / m);
    
      for (int k = 0; k < n; k += m) {
        var wReal = 1.0;
        var wImag = 0.0;
      
        for (int j = 0; j < m2; j++) {
          final t = k + j + m2;
          final u = k + j;
        
          final tReal = wReal * _real[t] - wImag * _imag[t];
          final tImag = wReal * _imag[t] + wImag * _real[t];
        
          _real[t] = _real[u] - tReal;
          _imag[t] = _imag[u] - tImag;
          _real[u] = _real[u] + tReal;
          _imag[u] = _imag[u] + tImag;
        
          final tempReal = wReal * wmReal - wImag * wmImag;
          final tempImag = wReal * wmImag + wImag * wmReal;
          wReal = tempReal;
          wImag = tempImag;
        }
      }
    }
  }
  
  void _bitReverse(int n) {
    final rev = List<int>.filled(n, 0);
    for (int i = 0; i < n; i++) {
      rev[i] = _reverseBits(i, _log2(n));
    }
  
    for (int i = 0; i < n; i++) {
      if (i < rev[i]) {
        final tempReal = _real[i];
        final tempImag = _imag[i];
        _real[i] = _real[rev[i]];
        _imag[i] = _imag[rev[i]];
        _real[rev[i]] = tempReal;
        _imag[rev[i]] = tempImag;
      }
    }
  }
  
  int _reverseBits(int x, int bits) {
    int result = 0;
    for (int i = 0; i < bits; i++) {
      result = (result << 1) | (x & 1);
      x >>= 1;
    }
    return result;
  }
  
  int _log2(int n) {
    int result = 0;
    while ((1 << result) < n) {
      result++;
    }
    return result;
  }
  
  Float32List getMagnitudes() {
    final n = _real.length;
    final magnitudes = Float32List(n ~/ 2);
  
    for (int i = 0; i < n ~/ 2; i++) {
      magnitudes[i] = sqrt(_real[i] * _real[i] + _imag[i] * _imag[i]) / n;
    }
  
    return magnitudes;
  }
}

📊 2.3 频谱数据平滑处理

原始 FFT 输出往往存在剧烈波动,需要进行平滑处理:

dart 复制代码
class SpectrumSmoother {
  final int bandCount;
  final double smoothingFactor;
  late final Float32List _previousValues;
  
  SpectrumSmoother({
    required this.bandCount,
    this.smoothingFactor = 0.8,
  }) {
    _previousValues = Float32List(bandCount);
  }
  
  Float32List smooth(Float32List input) {
    final output = Float32List(bandCount);
  
    for (int i = 0; i < bandCount; i++) {
      output[i] = input[i] * (1 - smoothingFactor) + 
                  _previousValues[i] * smoothingFactor;
      _previousValues[i] = output[i];
    }
  
    return output;
  }
  
  void reset() {
    _previousValues.fillRange(0, bandCount, 0);
  }
}

三、正弦波叠加:构建基础波形

🌊 3.1 单一正弦波生成

dart 复制代码
class SineWaveGenerator {
  final double frequency;
  final double amplitude;
  final double phase;
  final int sampleRate;
  
  SineWaveGenerator({
    required this.frequency,
    this.amplitude = 1.0,
    this.phase = 0,
    this.sampleRate = 44100,
  });
  
  Float32List generate(int sampleCount) {
    final samples = Float32List(sampleCount);
    final angularFrequency = 2 * pi * frequency;
  
    for (int i = 0; i < sampleCount; i++) {
      final t = i / sampleRate;
      samples[i] = amplitude * sin(angularFrequency * t + phase);
    }
  
    return samples;
  }
}

🎵 3.2 多频率正弦波叠加

dart 复制代码
class WaveSuperposition {
  final int sampleRate;
  final List<WaveComponent> components;
  
  WaveSuperposition({
    required this.sampleRate,
    required this.components,
  });
  
  Float32List generate(int sampleCount) {
    final result = Float32List(sampleCount);
  
    for (int i = 0; i < sampleCount; i++) {
      final t = i / sampleRate;
      double value = 0;
    
      for (final component in components) {
        value += component.amplitude * 
                 sin(2 * pi * component.frequency * t + component.phase);
      }
    
      result[i] = value;
    }
  
    // 归一化
    final maxAbs = result.reduce((a, b) => max(a.abs(), b.abs()));
    if (maxAbs > 0) {
      for (int i = 0; i < sampleCount; i++) {
        result[i] /= maxAbs;
      }
    }
  
    return result;
  }
}

class WaveComponent {
  final double frequency;
  final double amplitude;
  final double phase;
  
  WaveComponent({
    required this.frequency,
    required this.amplitude,
    this.phase = 0,
  });
}

🎨 3.3 波形可视化组件

dart 复制代码
class WaveformPainter extends CustomPainter {
  final Float32List samples;
  final Color color;
  final double strokeWidth;
  
  WaveformPainter({
    required this.samples,
    required this.color,
    this.strokeWidth = 2,
  });
  
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = color
      ..strokeWidth = strokeWidth
      ..style = PaintingStyle.stroke
      ..strokeCap = StrokeCap.round;
  
    final path = Path();
    final stepX = size.width / (samples.length - 1);
    final centerY = size.height / 2;
  
    for (int i = 0; i < samples.length; i++) {
      final x = i * stepX;
      final y = centerY + samples[i] * centerY * 0.9;
    
      if (i == 0) {
        path.moveTo(x, y);
      } else {
        path.lineTo(x, y);
      }
    }
  
    canvas.drawPath(path, paint);
  }
  
  @override
  bool shouldRepaint(covariant WaveformPainter oldDelegate) {
    return samples != oldDelegate.samples || color != oldDelegate.color;
  }
}

四、频谱能量场:动态可视化实现

📊 4.1 频谱柱状图组件

dart 复制代码
class SpectrumBarPainter extends CustomPainter {
  final Float32List magnitudes;
  final List<Color> colors;
  final double barWidth;
  final double gapWidth;
  final double cornerRadius;
  
  SpectrumBarPainter({
    required this.magnitudes,
    required this.colors,
    this.barWidth = 8,
    this.gapWidth = 2,
    this.cornerRadius = 4,
  });
  
  @override
  void paint(Canvas canvas, Size size) {
    final totalBars = magnitudes.length;
    final totalWidth = totalBars * barWidth + (totalBars - 1) * gapWidth;
    final startX = (size.width - totalWidth) / 2;
  
    for (int i = 0; i < totalBars; i++) {
      final magnitude = magnitudes[i].clamp(0.0, 1.0);
      final barHeight = magnitude * size.height;
    
      final x = startX + i * (barWidth + gapWidth);
      final y = size.height - barHeight;
    
      final colorIndex = (i / totalBars * colors.length).floor();
      final color = colors[colorIndex.clamp(0, colors.length - 1)];
    
      final rect = RRect.fromRectAndRadius(
        Rect.fromLTWH(x, y, barWidth, barHeight),
        Radius.circular(cornerRadius),
      );
    
      final paint = Paint()
        ..color = color
        ..style = PaintingStyle.fill;
    
      canvas.drawRRect(rect, paint);
    }
  }
  
  @override
  bool shouldRepaint(covariant SpectrumBarPainter oldDelegate) {
    return magnitudes != oldDelegate.magnitudes;
  }
}

🌈 4.2 渐变频谱可视化

dart 复制代码
class GradientSpectrumPainter extends CustomPainter {
  final Float32List magnitudes;
  final Gradient gradient;
  final double smoothness;
  
  GradientSpectrumPainter({
    required this.magnitudes,
    required this.gradient,
    this.smoothness = 0.3,
  });
  
  @override
  void paint(Canvas canvas, Size size) {
    final stepX = size.width / (magnitudes.length - 1);
  
    // 绘制填充区域
    final fillPath = Path();
    fillPath.moveTo(0, size.height);
  
    for (int i = 0; i < magnitudes.length; i++) {
      final x = i * stepX;
      final y = size.height - magnitudes[i] * size.height;
    
      if (i == 0) {
        fillPath.lineTo(x, y);
      } else {
        final prevX = (i - 1) * stepX;
        final prevY = size.height - magnitudes[i - 1] * size.height;
        final cpX = (prevX + x) / 2;
      
        fillPath.quadraticBezierTo(prevX, prevY, cpX, (prevY + y) / 2);
      }
    }
  
    fillPath.lineTo(size.width, size.height);
    fillPath.close();
  
    final fillPaint = Paint()
      ..shader = gradient.createShader(
        Rect.fromLTWH(0, 0, size.width, size.height),
      )
      ..style = PaintingStyle.fill;
  
    canvas.drawPath(fillPath, fillPaint);
  
    // 绘制描边
    final strokePath = Path();
  
    for (int i = 0; i < magnitudes.length; i++) {
      final x = i * stepX;
      final y = size.height - magnitudes[i] * size.height;
    
      if (i == 0) {
        strokePath.moveTo(x, y);
      } else {
        final prevX = (i - 1) * stepX;
        final prevY = size.height - magnitudes[i - 1] * size.height;
        final cpX = (prevX + x) / 2;
      
        strokePath.quadraticBezierTo(prevX, prevY, cpX, (prevY + y) / 2);
      }
    }
  
    final strokePaint = Paint()
      ..color = Colors.white.withOpacity(0.8)
      ..strokeWidth = 2
      ..style = PaintingStyle.stroke
      ..strokeCap = StrokeCap.round;
  
    canvas.drawPath(strokePath, strokePaint);
  }
  
  @override
  bool shouldRepaint(covariant GradientSpectrumPainter oldDelegate) {
    return magnitudes != oldDelegate.magnitudes;
  }
}

⚡ 4.3 能量场粒子效果

dart 复制代码
class EnergyParticle {
  double x;
  double y;
  double vx;
  double vy;
  double life;
  double maxLife;
  double size;
  Color color;
  
  EnergyParticle({
    required this.x,
    required this.y,
    required this.vx,
    required this.vy,
    required this.life,
    required this.maxLife,
    required this.size,
    required this.color,
  });
  
  bool get isAlive => life > 0;
  
  void update(double dt) {
    x += vx * dt;
    y += vy * dt;
    life -= dt;
    vy += 50 * dt; // 重力
  }
  
  double get opacity => (life / maxLife).clamp(0.0, 1.0);
}

class ParticleSystem {
  final List<EnergyParticle> particles = [];
  final Random random = Random();
  
  void emit({
    required double x,
    required double y,
    required double energy,
    required Color color,
    int count = 10,
  }) {
    for (int i = 0; i < count; i++) {
      final angle = random.nextDouble() * 2 * pi;
      final speed = energy * 50 + random.nextDouble() * 30;
    
      particles.add(EnergyParticle(
        x: x,
        y: y,
        vx: cos(angle) * speed,
        vy: sin(angle) * speed - 50,
        life: 0.5 + random.nextDouble() * 0.5,
        maxLife: 1.0,
        size: 2 + random.nextDouble() * 4,
        color: color,
      ));
    }
  }
  
  void update(double dt) {
    for (final particle in particles) {
      particle.update(dt);
    }
  
    particles.removeWhere((p) => !p.isAlive);
  }
  
  void draw(Canvas canvas) {
    for (final particle in particles) {
      final paint = Paint()
        ..color = particle.color.withOpacity(particle.opacity)
        ..style = PaintingStyle.fill;
    
      canvas.drawCircle(
        Offset(particle.x, particle.y),
        particle.size,
        paint,
      );
    }
  }
}

五、OpenHarmony 音频适配

🎙️ 5.1 音频采集适配器

dart 复制代码
import 'package:flutter/foundation.dart';
import 'dart:io';

class AudioAdapterFactory {
  static AudioAdapter create() {
    if (!kIsWeb && Platform.operatingSystem == 'ohos') {
      return OpenHarmonyAudioAdapter();
    }
    return MockAudioAdapter();
  }
}

abstract class AudioAdapter {
  Stream<Float32List> get audioStream;
  Future<void> start();
  Future<void> stop();
  void dispose();
}

class OpenHarmonyAudioAdapter implements AudioAdapter {
  static const _channel = EventChannel('com.example.app/audio');
  StreamSubscription<dynamic>? _subscription;
  final StreamController<Float32List> _controller = StreamController.broadcast();
  
  @override
  Stream<Float32List> get audioStream => _controller.stream;
  
  @override
  Future<void> start() async {
    _subscription = _channel.receiveBroadcastStream().listen(
      (data) {
        if (data is List) {
          final samples = Float32List.fromList(
            data.map((e) => (e as num).toDouble()).toList(),
          );
          _controller.add(samples);
        }
      },
      onError: (error) {
        debugPrint('Audio stream error: $error');
      },
    );
  }
  
  @override
  Future<void> stop() async {
    await _subscription?.cancel();
    _subscription = null;
  }
  
  @override
  void dispose() {
    stop();
    _controller.close();
  }
}

class MockAudioAdapter implements AudioAdapter {
  final StreamController<Float32List> _controller = StreamController.broadcast();
  Timer? _timer;
  final Random _random = Random();
  final FFTProcessor _fft = FFTProcessor(512);
  
  @override
  Stream<Float32List> get audioStream => _controller.stream;
  
  @override
  Future<void> start() async {
    _timer = Timer.periodic(const Duration(milliseconds: 50), (_) {
      final samples = Float32List(512);
      for (int i = 0; i < 512; i++) {
        samples[i] = _random.nextDouble() * 2 - 1;
      }
      _controller.add(samples);
    });
  }
  
  @override
  Future<void> stop() async {
    _timer?.cancel();
    _timer = null;
  }
  
  @override
  void dispose() {
    stop();
    _controller.close();
  }
}

📱 5.2 平台检测与适配

dart 复制代码
class PlatformInfo {
  static bool get isOpenHarmony {
    return !kIsWeb && Platform.operatingSystem == 'ohos';
  }
  
  static String get platformName {
    if (kIsWeb) return 'Web';
    return Platform.operatingSystem;
  }
  
  static String get platformVersion {
    if (kIsWeb) return 'Unknown';
    return Platform.operatingSystemVersion;
  }
}

六、完整示例代码

以下是完整的 FFT 频谱能量场示例代码:

dart 复制代码
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/foundation.dart';
import 'dart:async';
import 'dart:math';
import 'dart:io';
import 'dart:typed_data';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'FFT 频谱能量场',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.deepPurple,
          brightness: Brightness.dark,
        ),
        useMaterial3: true,
      ),
      home: const MusicVisualizerHomePage(),
      debugShowCheckedModeBanner: false,
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('🎵 FFT 频谱能量场'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          _buildSectionCard(
            context,
            title: '正弦波叠加',
            description: '多频率波形可视化',
            icon: Icons.waves,
            color: Colors.blue,
            onTap: () => Navigator.push(
              context,
              MaterialPageRoute(builder: (_) => const SineWaveDemo()),
            ),
          ),
          _buildSectionCard(
            context,
            title: '频谱柱状图',
            description: 'FFT 频谱分析可视化',
            icon: Icons.bar_chart,
            color: Colors.green,
            onTap: () => Navigator.push(
              context,
              MaterialPageRoute(builder: (_) => const SpectrumBarDemo()),
            ),
          ),
          _buildSectionCard(
            context,
            title: '渐变频谱',
            description: '平滑渐变波形',
            icon: Icons.gradient,
            color: Colors.purple,
            onTap: () => Navigator.push(
              context,
              MaterialPageRoute(builder: (_) => const GradientSpectrumDemo()),
            ),
          ),
          _buildSectionCard(
            context,
            title: '能量粒子',
            description: '粒子喷射效果',
            icon: Icons.blur_on,
            color: Colors.orange,
            onTap: () => Navigator.push(
              context,
              MaterialPageRoute(builder: (_) => const ParticleSpectrumDemo()),
            ),
          ),
          _buildSectionCard(
            context,
            title: '综合演示',
            description: '完整频谱能量场',
            icon: Icons.music_note,
            color: Colors.red,
            onTap: () => Navigator.push(
              context,
              MaterialPageRoute(builder: (_) => const FullSpectrumDemo()),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildSectionCard(
    BuildContext context, {
    required String title,
    required String description,
    required IconData icon,
    required Color color,
    required VoidCallback onTap,
  }) {
    return Card(
      margin: const EdgeInsets.only(bottom: 12),
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
      child: InkWell(
        onTap: onTap,
        borderRadius: BorderRadius.circular(16),
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Row(
            children: [
              Container(
                width: 56,
                height: 56,
                decoration: BoxDecoration(
                  color: color.withOpacity(0.1),
                  borderRadius: BorderRadius.circular(12),
                ),
                child: Icon(icon, color: color, size: 28),
              ),
              const SizedBox(width: 16),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      title,
                      style: const TextStyle(
                        fontSize: 16,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    const SizedBox(height: 4),
                    Text(
                      description,
                      style: TextStyle(color: Colors.grey[600], fontSize: 14),
                    ),
                  ],
                ),
              ),
              Icon(Icons.chevron_right, color: Colors.grey[400]),
            ],
          ),
        ),
      ),
    );
  }
}

class FFTProcessor {
  final int size;
  late final Float32List _real;
  late final Float32List _imag;
  
  FFTProcessor(this.size) {
    _real = Float32List(size);
    _imag = Float32List(size);
  }
  
  void compute(Float32List input) {
    final n = input.length;
  
    for (int i = 0; i < n; i++) {
      _real[i] = input[i];
      _imag[i] = 0;
    }
  
    _bitReverse(n);
  
    for (int s = 1; s <= _log2(n); s++) {
      final m = 1 << s;
      final m2 = m >> 1;
      final wmReal = cos(2 * pi / m);
      final wmImag = -sin(2 * pi / m);
    
      for (int k = 0; k < n; k += m) {
        var wReal = 1.0;
        var wImag = 0.0;
      
        for (int j = 0; j < m2; j++) {
          final t = k + j + m2;
          final u = k + j;
        
          final tReal = wReal * _real[t] - wImag * _imag[t];
          final tImag = wReal * _imag[t] + wImag * _real[t];
        
          _real[t] = _real[u] - tReal;
          _imag[t] = _imag[u] - tImag;
          _real[u] = _real[u] + tReal;
          _imag[u] = _imag[u] + tImag;
        
          final tempReal = wReal * wmReal - wImag * wmImag;
          final tempImag = wReal * wmImag + wImag * wmReal;
          wReal = tempReal;
          wImag = tempImag;
        }
      }
    }
  }
  
  void _bitReverse(int n) {
    for (int i = 0; i < n; i++) {
      final rev = _reverseBits(i, _log2(n));
      if (i < rev) {
        final tempReal = _real[i];
        final tempImag = _imag[i];
        _real[i] = _real[rev];
        _imag[i] = _imag[rev];
        _real[rev] = tempReal;
        _imag[rev] = tempImag;
      }
    }
  }
  
  int _reverseBits(int x, int bits) {
    int result = 0;
    for (int i = 0; i < bits; i++) {
      result = (result << 1) | (x & 1);
      x >>= 1;
    }
    return result;
  }
  
  int _log2(int n) {
    int result = 0;
    while ((1 << result) < n) {
      result++;
    }
    return result;
  }
  
  Float32List getMagnitudes() {
    final n = _real.length;
    final magnitudes = Float32List(n ~/ 2);
  
    for (int i = 0; i < n ~/ 2; i++) {
      magnitudes[i] = sqrt(_real[i] * _real[i] + _imag[i] * _imag[i]) / n;
    }
  
    return magnitudes;
  }
}

class SpectrumSmoother {
  final int bandCount;
  final double smoothingFactor;
  late final Float32List _previousValues;
  
  SpectrumSmoother({
    required this.bandCount,
    this.smoothingFactor = 0.8,
  }) {
    _previousValues = Float32List(bandCount);
  }
  
  Float32List smooth(Float32List input) {
    final output = Float32List(bandCount);
  
    for (int i = 0; i < bandCount; i++) {
      output[i] = input[i] * (1 - smoothingFactor) + 
                  _previousValues[i] * smoothingFactor;
      _previousValues[i] = output[i];
    }
  
    return output;
  }
}

class SineWaveDemo extends StatefulWidget {
  const SineWaveDemo({super.key});

  @override
  State<SineWaveDemo> createState() => _SineWaveDemoState();
}

class _SineWaveDemoState extends State<SineWaveDemo> with SingleTickerProviderStateMixin {
  late final AnimationController _controller;
  Float32List _samples = Float32List(256);
  double _frequency1 = 2.0;
  double _frequency2 = 5.0;
  double _frequency3 = 11.0;
  double _amplitude1 = 1.0;
  double _amplitude2 = 0.5;
  double _amplitude3 = 0.3;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 10),
    )..repeat();
  
    _controller.addListener(_updateWave);
  }

  void _updateWave() {
    final t = _controller.value * 2 * pi;
  
    for (int i = 0; i < 256; i++) {
      final x = i / 256 * 4 * pi;
      _samples[i] = _amplitude1 * sin(_frequency1 * x + t) +
                    _amplitude2 * sin(_frequency2 * x + t * 1.5) +
                    _amplitude3 * sin(_frequency3 * x + t * 2);
    }
  
    final maxAbs = _samples.reduce((a, b) => max(a.abs(), b.abs()));
    if (maxAbs > 0) {
      for (int i = 0; i < 256; i++) {
        _samples[i] /= maxAbs;
      }
    }
  
    setState(() {});
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('正弦波叠加')),
      body: Column(
        children: [
          Expanded(
            child: Container(
              color: Colors.black,
              child: CustomPaint(
                painter: WaveformPainter(
                  samples: _samples,
                  color: Colors.cyan,
                  strokeWidth: 2,
                ),
                size: Size.infinite,
              ),
            ),
          ),
          Container(
            padding: const EdgeInsets.all(16),
            child: Column(
              children: [
                _buildSlider('频率 1', _frequency1, 1, 10, (v) => _frequency1 = v),
                _buildSlider('振幅 1', _amplitude1, 0, 1, (v) => _amplitude1 = v),
                _buildSlider('频率 2', _frequency2, 1, 20, (v) => _frequency2 = v),
                _buildSlider('振幅 2', _amplitude2, 0, 1, (v) => _amplitude2 = v),
                _buildSlider('频率 3', _frequency3, 1, 30, (v) => _frequency3 = v),
                _buildSlider('振幅 3', _amplitude3, 0, 1, (v) => _amplitude3 = v),
              ],
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildSlider(String label, double value, double min, double max, Function(double) onChanged) {
    return Row(
      children: [
        SizedBox(width: 80, child: Text(label)),
        Expanded(
          child: Slider(
            value: value,
            min: min,
            max: max,
            onChanged: (v) => setState(() => onChanged(v)),
          ),
        ),
        SizedBox(width: 50, child: Text(value.toStringAsFixed(2))),
      ],
    );
  }
}

class WaveformPainter extends CustomPainter {
  final Float32List samples;
  final Color color;
  final double strokeWidth;

  WaveformPainter({
    required this.samples,
    required this.color,
    required this.strokeWidth,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = color
      ..strokeWidth = strokeWidth
      ..style = PaintingStyle.stroke
      ..strokeCap = StrokeCap.round;

    final path = Path();
    final stepX = size.width / (samples.length - 1);
    final centerY = size.height / 2;

    for (int i = 0; i < samples.length; i++) {
      final x = i * stepX;
      final y = centerY + samples[i] * centerY * 0.9;

      if (i == 0) {
        path.moveTo(x, y);
      } else {
        path.lineTo(x, y);
      }
    }

    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(covariant WaveformPainter oldDelegate) {
    return samples != oldDelegate.samples;
  }
}

class SpectrumBarDemo extends StatefulWidget {
  const SpectrumBarDemo({super.key});

  @override
  State<SpectrumBarDemo> createState() => _SpectrumBarDemoState();
}

class _SpectrumBarDemoState extends State<SpectrumBarDemo> with SingleTickerProviderStateMixin {
  late final AnimationController _controller;
  final FFTProcessor _fft = FFTProcessor(256);
  final SpectrumSmoother _smoother = SpectrumSmoother(bandCount: 32);
  final Random _random = Random();
  Float32List _magnitudes = Float32List(32);

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 50),
    )..repeat();
  
    _controller.addListener(_updateSpectrum);
  }

  void _updateSpectrum() {
    final samples = Float32List(256);
    for (int i = 0; i < 256; i++) {
      samples[i] = _random.nextDouble() * 2 - 1;
    }
  
    _fft.compute(samples);
    final rawMagnitudes = _fft.getMagnitudes();
  
    final bandSize = rawMagnitudes.length ~/ 32;
    final bands = Float32List(32);
  
    for (int i = 0; i < 32; i++) {
      double sum = 0;
      for (int j = 0; j < bandSize; j++) {
        sum += rawMagnitudes[i * bandSize + j];
      }
      bands[i] = sum / bandSize * 5;
    }
  
    _magnitudes = _smoother.smooth(bands);
    setState(() {});
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('频谱柱状图')),
      body: Container(
        color: Colors.black,
        child: CustomPaint(
          painter: SpectrumBarPainter(
            magnitudes: _magnitudes,
            colors: [
              Colors.blue,
              Colors.cyan,
              Colors.green,
              Colors.yellow,
              Colors.orange,
              Colors.red,
            ],
          ),
          size: Size.infinite,
        ),
      ),
    );
  }
}

class SpectrumBarPainter extends CustomPainter {
  final Float32List magnitudes;
  final List<Color> colors;
  final double barWidth;
  final double gapWidth;
  final double cornerRadius;

  SpectrumBarPainter({
    required this.magnitudes,
    required this.colors,
    this.barWidth = 8,
    this.gapWidth = 2,
    this.cornerRadius = 4,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final totalBars = magnitudes.length;
    final totalWidth = totalBars * barWidth + (totalBars - 1) * gapWidth;
    final startX = (size.width - totalWidth) / 2;

    for (int i = 0; i < totalBars; i++) {
      final magnitude = magnitudes[i].clamp(0.0, 1.0);
      final barHeight = magnitude * size.height * 0.9;

      final x = startX + i * (barWidth + gapWidth);
      final y = size.height - barHeight;

      final colorIndex = (i / totalBars * colors.length).floor();
      final color = colors[colorIndex.clamp(0, colors.length - 1)];

      final rect = RRect.fromRectAndRadius(
        Rect.fromLTWH(x, y, barWidth, barHeight),
        Radius.circular(cornerRadius),
      );

      final paint = Paint()
        ..color = color
        ..style = PaintingStyle.fill;

      canvas.drawRRect(rect, paint);
    }
  }

  @override
  bool shouldRepaint(covariant SpectrumBarPainter oldDelegate) {
    return magnitudes != oldDelegate.magnitudes;
  }
}

class GradientSpectrumDemo extends StatefulWidget {
  const GradientSpectrumDemo({super.key});

  @override
  State<GradientSpectrumDemo> createState() => _GradientSpectrumDemoState();
}

class _GradientSpectrumDemoState extends State<GradientSpectrumDemo> with SingleTickerProviderStateMixin {
  late final AnimationController _controller;
  final FFTProcessor _fft = FFTProcessor(256);
  final SpectrumSmoother _smoother = SpectrumSmoother(bandCount: 64);
  final Random _random = Random();
  Float32List _magnitudes = Float32List(64);

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 50),
    )..repeat();
  
    _controller.addListener(_updateSpectrum);
  }

  void _updateSpectrum() {
    final samples = Float32List(256);
    for (int i = 0; i < 256; i++) {
      samples[i] = _random.nextDouble() * 2 - 1;
    }
  
    _fft.compute(samples);
    final rawMagnitudes = _fft.getMagnitudes();
  
    final bandSize = rawMagnitudes.length ~/ 64;
    final bands = Float32List(64);
  
    for (int i = 0; i < 64; i++) {
      double sum = 0;
      for (int j = 0; j < bandSize; j++) {
        final idx = i * bandSize + j;
        if (idx < rawMagnitudes.length) {
          sum += rawMagnitudes[idx];
        }
      }
      bands[i] = sum / bandSize * 5;
    }
  
    _magnitudes = _smoother.smooth(bands);
    setState(() {});
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('渐变频谱')),
      body: Container(
        decoration: const BoxDecoration(
          gradient: LinearGradient(
            begin: Alignment.topCenter,
            end: Alignment.bottomCenter,
            colors: [Colors.black, Colors.deepPurple],
          ),
        ),
        child: CustomPaint(
          painter: GradientSpectrumPainter(
            magnitudes: _magnitudes,
            gradient: const LinearGradient(
              begin: Alignment.bottomCenter,
              end: Alignment.topCenter,
              colors: [Colors.purple, Colors.pink, Colors.orange, Colors.yellow],
            ),
          ),
          size: Size.infinite,
        ),
      ),
    );
  }
}

class GradientSpectrumPainter extends CustomPainter {
  final Float32List magnitudes;
  final Gradient gradient;

  GradientSpectrumPainter({
    required this.magnitudes,
    required this.gradient,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final stepX = size.width / (magnitudes.length - 1);

    final fillPath = Path();
    fillPath.moveTo(0, size.height);

    for (int i = 0; i < magnitudes.length; i++) {
      final x = i * stepX;
      final y = size.height - magnitudes[i].clamp(0.0, 1.0) * size.height * 0.9;

      if (i == 0) {
        fillPath.lineTo(x, y);
      } else {
        final prevX = (i - 1) * stepX;
        final prevY = size.height - magnitudes[i - 1].clamp(0.0, 1.0) * size.height * 0.9;
        final cpX = (prevX + x) / 2;

        fillPath.quadraticBezierTo(prevX, prevY, cpX, (prevY + y) / 2);
      }
    }

    fillPath.lineTo(size.width, size.height);
    fillPath.close();

    final fillPaint = Paint()
      ..shader = gradient.createShader(
        Rect.fromLTWH(0, 0, size.width, size.height),
      )
      ..style = PaintingStyle.fill;

    canvas.drawPath(fillPath, fillPaint);

    final strokePath = Path();

    for (int i = 0; i < magnitudes.length; i++) {
      final x = i * stepX;
      final y = size.height - magnitudes[i].clamp(0.0, 1.0) * size.height * 0.9;

      if (i == 0) {
        strokePath.moveTo(x, y);
      } else {
        final prevX = (i - 1) * stepX;
        final prevY = size.height - magnitudes[i - 1].clamp(0.0, 1.0) * size.height * 0.9;
        final cpX = (prevX + x) / 2;

        strokePath.quadraticBezierTo(prevX, prevY, cpX, (prevY + y) / 2);
      }
    }

    final strokePaint = Paint()
      ..color = Colors.white.withOpacity(0.8)
      ..strokeWidth = 2
      ..style = PaintingStyle.stroke
      ..strokeCap = StrokeCap.round;

    canvas.drawPath(strokePath, strokePaint);
  }

  @override
  bool shouldRepaint(covariant GradientSpectrumPainter oldDelegate) {
    return magnitudes != oldDelegate.magnitudes;
  }
}

class ParticleSpectrumDemo extends StatefulWidget {
  const ParticleSpectrumDemo({super.key});

  @override
  State<ParticleSpectrumDemo> createState() => _ParticleSpectrumDemoState();
}

class _ParticleSpectrumDemoState extends State<ParticleSpectrumDemo> with SingleTickerProviderStateMixin {
  late final AnimationController _controller;
  final FFTProcessor _fft = FFTProcessor(256);
  final SpectrumSmoother _smoother = SpectrumSmoother(bandCount: 16);
  final ParticleSystem _particleSystem = ParticleSystem();
  final Random _random = Random();
  Float32List _magnitudes = Float32List(16);
  double _lastTime = 0;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 1),
    )..repeat();
  
    _controller.addListener(_update);
  }

  void _update() {
    final currentTime = DateTime.now().millisecondsSinceEpoch / 1000.0;
    final dt = _lastTime > 0 ? currentTime - _lastTime : 0.016;
    _lastTime = currentTime;
  
    final samples = Float32List(256);
    for (int i = 0; i < 256; i++) {
      samples[i] = _random.nextDouble() * 2 - 1;
    }
  
    _fft.compute(samples);
    final rawMagnitudes = _fft.getMagnitudes();
  
    final bandSize = rawMagnitudes.length ~/ 16;
    final bands = Float32List(16);
  
    for (int i = 0; i < 16; i++) {
      double sum = 0;
      for (int j = 0; j < bandSize; j++) {
        final idx = i * bandSize + j;
        if (idx < rawMagnitudes.length) {
          sum += rawMagnitudes[idx];
        }
      }
      bands[i] = sum / bandSize * 5;
    }
  
    _magnitudes = _smoother.smooth(bands);
    _particleSystem.update(dt);
  
    setState(() {});
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('能量粒子')),
      body: Container(
        color: Colors.black,
        child: LayoutBuilder(
          builder: (context, constraints) {
            final barWidth = constraints.maxWidth / 16;
          
            for (int i = 0; i < 16; i++) {
              if (_magnitudes[i] > 0.3 && _random.nextDouble() < 0.3) {
                _particleSystem.emit(
                  x: i * barWidth + barWidth / 2,
                  y: constraints.maxHeight - _magnitudes[i] * constraints.maxHeight * 0.9,
                  energy: _magnitudes[i],
                  color: HSVColor.fromAHSV(1, i / 16 * 360, 1, 1).toColor(),
                  count: (_magnitudes[i] * 5).round(),
                );
              }
            }
          
            return CustomPaint(
              painter: ParticleSpectrumPainter(
                magnitudes: _magnitudes,
                particleSystem: _particleSystem,
              ),
              size: Size.infinite,
            );
          },
        ),
      ),
    );
  }
}

class EnergyParticle {
  double x;
  double y;
  double vx;
  double vy;
  double life;
  double maxLife;
  double size;
  Color color;

  EnergyParticle({
    required this.x,
    required this.y,
    required this.vx,
    required this.vy,
    required this.life,
    required this.maxLife,
    required this.size,
    required this.color,
  });

  bool get isAlive => life > 0;

  void update(double dt) {
    x += vx * dt;
    y += vy * dt;
    life -= dt;
    vy += 50 * dt;
  }

  double get opacity => (life / maxLife).clamp(0.0, 1.0);
}

class ParticleSystem {
  final List<EnergyParticle> particles = [];
  final Random random = Random();

  void emit({
    required double x,
    required double y,
    required double energy,
    required Color color,
    int count = 10,
  }) {
    for (int i = 0; i < count; i++) {
      final angle = random.nextDouble() * 2 * pi;
      final speed = energy * 50 + random.nextDouble() * 30;

      particles.add(EnergyParticle(
        x: x,
        y: y,
        vx: cos(angle) * speed,
        vy: sin(angle) * speed - 50,
        life: 0.5 + random.nextDouble() * 0.5,
        maxLife: 1.0,
        size: 2 + random.nextDouble() * 4,
        color: color,
      ));
    }
  }

  void update(double dt) {
    for (final particle in particles) {
      particle.update(dt);
    }

    particles.removeWhere((p) => !p.isAlive);
  }

  void draw(Canvas canvas) {
    for (final particle in particles) {
      final paint = Paint()
        ..color = particle.color.withOpacity(particle.opacity)
        ..style = PaintingStyle.fill;

      canvas.drawCircle(
        Offset(particle.x, particle.y),
        particle.size,
        paint,
      );
    }
  }
}

class ParticleSpectrumPainter extends CustomPainter {
  final Float32List magnitudes;
  final ParticleSystem particleSystem;

  ParticleSpectrumPainter({
    required this.magnitudes,
    required this.particleSystem,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final barWidth = size.width / magnitudes.length;

    for (int i = 0; i < magnitudes.length; i++) {
      final magnitude = magnitudes[i].clamp(0.0, 1.0);
      final barHeight = magnitude * size.height * 0.9;

      final x = i * barWidth;
      final y = size.height - barHeight;

      final hue = i / magnitudes.length * 360;
      final color = HSVColor.fromAHSV(1, hue, 1, 1).toColor();

      final rect = RRect.fromRectAndRadius(
        Rect.fromLTWH(x + 2, y, barWidth - 4, barHeight),
        const Radius.circular(4),
      );

      final paint = Paint()
        ..color = color
        ..style = PaintingStyle.fill;

      canvas.drawRRect(rect, paint);
    }

    particleSystem.draw(canvas);
  }

  @override
  bool shouldRepaint(covariant ParticleSpectrumPainter oldDelegate) {
    return magnitudes != oldDelegate.magnitudes;
  }
}

class FullSpectrumDemo extends StatefulWidget {
  const FullSpectrumDemo({super.key});

  @override
  State<FullSpectrumDemo> createState() => _FullSpectrumDemoState();
}

class _FullSpectrumDemoState extends State<FullSpectrumDemo> with SingleTickerProviderStateMixin {
  late final AnimationController _controller;
  final FFTProcessor _fft = FFTProcessor(256);
  final SpectrumSmoother _smoother = SpectrumSmoother(bandCount: 32);
  final ParticleSystem _particleSystem = ParticleSystem();
  final Random _random = Random();
  Float32List _magnitudes = Float32List(32);
  Float32List _waveform = Float32List(256);
  double _lastTime = 0;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 16),
    )..repeat();
  
    _controller.addListener(_update);
  }

  void _update() {
    final currentTime = DateTime.now().millisecondsSinceEpoch / 1000.0;
    final dt = _lastTime > 0 ? currentTime - _lastTime : 0.016;
    _lastTime = currentTime;
  
    final samples = Float32List(256);
    for (int i = 0; i < 256; i++) {
      samples[i] = _random.nextDouble() * 2 - 1;
    }
  
    _waveform = Float32List.fromList(samples);
  
    _fft.compute(samples);
    final rawMagnitudes = _fft.getMagnitudes();
  
    final bandSize = rawMagnitudes.length ~/ 32;
    final bands = Float32List(32);
  
    for (int i = 0; i < 32; i++) {
      double sum = 0;
      for (int j = 0; j < bandSize; j++) {
        final idx = i * bandSize + j;
        if (idx < rawMagnitudes.length) {
          sum += rawMagnitudes[idx];
        }
      }
      bands[i] = sum / bandSize * 5;
    }
  
    _magnitudes = _smoother.smooth(bands);
    _particleSystem.update(dt);
  
    setState(() {});
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('综合演示')),
      body: Container(
        decoration: const BoxDecoration(
          gradient: LinearGradient(
            begin: Alignment.topCenter,
            end: Alignment.bottomCenter,
            colors: [Colors.black, Colors.deepPurple, Colors.black],
          ),
        ),
        child: Column(
          children: [
            Expanded(
              flex: 2,
              child: CustomPaint(
                painter: WaveformPainter(
                  samples: _waveform,
                  color: Colors.cyan,
                  strokeWidth: 1,
                ),
                size: Size.infinite,
              ),
            ),
            Expanded(
              flex: 3,
              child: LayoutBuilder(
                builder: (context, constraints) {
                  final barWidth = constraints.maxWidth / 32;
                
                  for (int i = 0; i < 32; i++) {
                    if (_magnitudes[i] > 0.4 && _random.nextDouble() < 0.2) {
                      _particleSystem.emit(
                        x: i * barWidth + barWidth / 2,
                        y: constraints.maxHeight - _magnitudes[i] * constraints.maxHeight * 0.9,
                        energy: _magnitudes[i],
                        color: HSVColor.fromAHSV(1, i / 32 * 360, 1, 1).toColor(),
                        count: (_magnitudes[i] * 3).round(),
                      );
                    }
                  }
                
                  return CustomPaint(
                    painter: FullSpectrumPainter(
                      magnitudes: _magnitudes,
                      particleSystem: _particleSystem,
                    ),
                    size: Size.infinite,
                  );
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class FullSpectrumPainter extends CustomPainter {
  final Float32List magnitudes;
  final ParticleSystem particleSystem;

  FullSpectrumPainter({
    required this.magnitudes,
    required this.particleSystem,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final barWidth = size.width / magnitudes.length;

    for (int i = 0; i < magnitudes.length; i++) {
      final magnitude = magnitudes[i].clamp(0.0, 1.0);
      final barHeight = magnitude * size.height * 0.9;

      final x = i * barWidth;
      final y = size.height - barHeight;

      final hue = i / magnitudes.length * 360;
      final color = HSVColor.fromAHSV(0.8, hue, 1, 1).toColor();

      final rect = RRect.fromRectAndRadius(
        Rect.fromLTWH(x + 1, y, barWidth - 2, barHeight),
        const Radius.circular(2),
      );

      final paint = Paint()
        ..color = color
        ..style = PaintingStyle.fill;

      canvas.drawRRect(rect, paint);
    }

    particleSystem.draw(canvas);
  }

  @override
  bool shouldRepaint(covariant FullSpectrumPainter oldDelegate) {
    return magnitudes != oldDelegate.magnitudes;
  }
}

七、总结

本文深入探讨了 Flutter for OpenHarmony 的 FFT 频谱能量场实现,从数学原理到代码实践,涵盖了以下核心内容:

📚 核心知识点回顾

  1. FFT 原理:理解快速傅里叶变换的数学基础和算法优化
  2. 正弦波叠加:多频率波形的生成与可视化
  3. 频谱分析:时域信号到频域信号的转换
  4. 平滑处理:频谱数据的平滑算法
  5. 粒子系统:能量场粒子效果实现
  6. 平台适配:OpenHarmony 音频采集适配

🎯 最佳实践要点

  • 使用 Float32List 提高数值计算性能
  • 合理设置平滑因子避免频谱抖动
  • 粒子数量需要控制避免性能问题
  • 动画帧率控制在 60fps 以内

🚀 进阶方向

  • 实现真实音频输入的频谱分析
  • 探索更复杂的可视化效果
  • 优化大数据量场景的渲染性能
  • 实现音频特征的实时提取

通过掌握这些技术,你可以构建出炫酷的音乐可视化应用,为用户带来沉浸式的视听体验。


💡 提示 :在实际项目中,建议使用专业的音频处理库如 fftea 来实现 FFT,性能更优。

相关推荐
2601_949593652 小时前
Flutter for Harmony 跨平台开发实战:德劳内三角剖分——点集连接的几何美学
flutter
lili-felicity2 小时前
基础入门 Flutter for OpenHarmony:三方库实战 flutter_lifecycle_detector 生命周期检测详解
flutter
2601_949593652 小时前
Flutter for Harmony 跨平台开发实战:双曲几何与庞加莱圆盘——非欧空间的视觉映射
flutter
ChinaDragonDreamer2 小时前
HarmonyOS:知识点总结(一)
harmonyos·鸿蒙
松叶似针2 小时前
Flutter三方库适配OpenHarmony【doc_text】— parseDocxXml:正则驱动的 XML 文本提取
xml·flutter
张雨zy2 小时前
HarmonyOS鸿蒙 Preference 数据存储:简单实用的本地存储方案
华为·harmonyos
lili-felicity2 小时前
基础入门 Flutter for OpenHarmony:三方库实战 flutter_phone_direct_caller 电话拨号详解
flutter
不爱吃糖的程序媛3 小时前
Flutter-OH 插件适配 HarmonyOS 实战:以屏幕方向控制为例
flutter·华为·harmonyos
松叶似针3 小时前
Flutter三方库适配OpenHarmony【doc_text】— 文件格式路由:.doc 与 .docx 的分流策略
flutter·harmonyos