
欢迎加入开源鸿蒙跨平台社区: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 频谱能量场实现,从数学原理到代码实践,涵盖了以下核心内容:
📚 核心知识点回顾
- FFT 原理:理解快速傅里叶变换的数学基础和算法优化
- 正弦波叠加:多频率波形的生成与可视化
- 频谱分析:时域信号到频域信号的转换
- 平滑处理:频谱数据的平滑算法
- 粒子系统:能量场粒子效果实现
- 平台适配:OpenHarmony 音频采集适配
🎯 最佳实践要点
- 使用 Float32List 提高数值计算性能
- 合理设置平滑因子避免频谱抖动
- 粒子数量需要控制避免性能问题
- 动画帧率控制在 60fps 以内
🚀 进阶方向
- 实现真实音频输入的频谱分析
- 探索更复杂的可视化效果
- 优化大数据量场景的渲染性能
- 实现音频特征的实时提取
通过掌握这些技术,你可以构建出炫酷的音乐可视化应用,为用户带来沉浸式的视听体验。
💡 提示 :在实际项目中,建议使用专业的音频处理库如
fftea来实现 FFT,性能更优。