项目背景
在鸿蒙6.0的多设备协同场景下,音频应用需要同时支持播放控制和实时可视化效果。本文基于API 21和鸿蒙6.0+,使用Flutter构建一个支持音频播放、频谱可视化、歌词同步的跨端播放器,展示三方库audioplayers和flutter_sound的鸿蒙适配方案。

项目概述
项目名称:HarmonyMusicVisualizer
核心功能:

本地音频文件播放(支持MP3/AAC/WAV)

实时音频频谱可视化(使用fft库计算)

歌词LRC同步显示

鸿蒙6.0音频焦点的原生处理

元服务卡片控制播放

环境配置
yaml

pubspec.yaml

dependencies:
flutter:
sdk: flutter
audioplayers: ^5.2.1 # 音频播放核心库(鸿蒙适配版)
flutter_sound: ^9.2.1 # 音频录制与波形生成
fft: ^0.3.0 # 傅里叶变换计算频谱
path_provider: ^2.1.0 # 文件路径管理
permission_handler: ^11.3.0 # 权限管理
核心实现

  1. 音频播放服务 lib/services/audio_service.dart
    dart
    import ‘package:audioplayers/audioplayers.dart’;
    import ‘dart:typed_data’;
    import ‘dart:math’ as math;

class AudioPlaybackService {
static final AudioPlayer _player = AudioPlayer();
static final List _amplitudes = List.filled(256, 0.0);

/// 播放本地音频文件
static Future playLocal(String filePath) async {
await _player.play(DeviceFileSource(filePath));
_startAmplitudeMonitor();
}

/// 获取实时频谱数据(用于可视化)
static Stream<List> getSpectrumStream() async* {
while (_player.state == PlayerState.playing) {
await Future.delayed(Duration(milliseconds: 50));

  // 模拟获取音频振幅(实际需结合fft)
  final List<double> spectrum = List.generate(256, (i) {
    return math.sin(DateTime.now().millisecondsSinceEpoch / 1000 * (i + 1)) * 0.5 + 0.5;
  });
  yield spectrum;
}

}

/// 获取播放位置
static Stream getPositionStream() => _player.onPositionChanged;

/// 设置音量(0.0-1.0)
static Future setVolume(double volume) async {
await _player.setVolume(volume);
}

static void _startAmplitudeMonitor() {
// 监听音频振幅(鸿蒙原生通道可获取真实值)
_player.onPlayerStateChanged.listen((state) {
if (state == PlayerState.completed) {
_player.stop();
}
});
}
}
2. 频谱可视化Widget lib/widgets/spectrum_visualizer.dart
dart
import ‘package:flutter/material.dart’;
import ‘dart:math’ as math;

class SpectrumVisualizer extends StatefulWidget {
final Stream<List> spectrumStream;
final Color primaryColor;
final double barWidth;

const SpectrumVisualizer({
super.key,
required this.spectrumStream,
this.primaryColor = Colors.cyan,
this.barWidth = 4.0,
});

@override
State createState() => _SpectrumVisualizerState();
}

class _SpectrumVisualizerState extends State
with SingleTickerProviderStateMixin {
List _spectrum = List.filled(128, 0.0);
late AnimationController _animationController;

@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: Duration(milliseconds: 50),
)…repeat();

widget.spectrumStream.listen((data) {
  setState(() {
    _spectrum = data.take(128).toList();
  });
});

}

@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return CustomPaint(
painter: SpectrumPainter(
spectrum: _spectrum,
primaryColor: widget.primaryColor,
barWidth: widget.barWidth,
),
size: Size.infinite,
);
},
);
}

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

class SpectrumPainter extends CustomPainter {
final List spectrum;
final Color primaryColor;
final double barWidth;

SpectrumPainter({
required this.spectrum,
required this.primaryColor,
required this.barWidth,
});

@override
void paint(Canvas canvas, Size size) {
final double barSpacing = barWidth + 2;
final int barCount = spectrum.length;
final double availableWidth = size.width - 20;
final double actualBarWidth = (availableWidth / barCount) - 2;
final double maxHeight = size.height - 40;

for (int i = 0; i < barCount; i++) {
  double normalized = spectrum[i].clamp(0.0, 1.0);
  double height = normalized * maxHeight;
  double x = 10 + i * (actualBarWidth + 2);
  double y = size.height - 20 - height;
  
  // 根据振幅渐变颜色
  final Color barColor = Color.lerp(
    primaryColor.withOpacity(0.4),
    primaryColor,
    normalized,
  )!;
  
  final Rect rect = Rect.fromLTWH(x, y, actualBarWidth, height);
  final RRect rRect = RRect.fromRectAndRadius(rect, Radius.circular(2));
  
  canvas.drawRRect(rRect, Paint()..color = barColor);
}

}

@override
bool shouldRepaint(SpectrumPainter oldDelegate) {
return oldDelegate.spectrum != spectrum;
}
}
3. 鸿蒙原生音频焦点 ohos/entry/src/main/ets/plugins/AudioFocusPlugin.ets
typescript
import { audio } from ‘@kit.AudioKit’;
import { MethodChannel, FlutterPlugin, AbilityPlugin } from ‘@ohos/flutter_ohos’;
import { common } from ‘@kit.AbilityKit’;

export class AudioFocusPlugin implements FlutterPlugin {
private channel: MethodChannel | null = null;
private audioManager: audio.AudioManager | null = null;
private audioFocusRequest: audio.AudioFocusRequest | null = null;

async onAttachedToEngine(binding: AbilityPlugin): Promise {
this.channel = new MethodChannel(binding, “com.audioviz/focus”);
this.audioManager = await audio.getAudioManager();

this.channel.setMethodCallHandler((call, result) => {
  switch(call.method) {
    case 'requestFocus':
      this.requestAudioFocus(result);
      break;
    case 'abandonFocus':
      this.abandonAudioFocus(result);
      break;
    default:
      result.notImplemented();
  }
});

}

private async requestAudioFocus(result: any) {
// 鸿蒙6.0音频焦点请求(API 21)
const focusRequest: audio.AudioFocusRequest = {
sourceType: audio.AudioSourceType.AUDIO_SOURCE_TYPE_MEDIA,
focusType: audio.AudioFocusType.AUDIO_FOCUS_TYPE_GAIN,
durationHint: audio.AudioFocusDurationHint.AUDIO_FOCUS_DURATION_MS,
onInterrupt: (interruptEvent) => {
// 处理音频中断(电话、闹钟等)
this.channel?.invokeMethod(‘onInterrupt’, {
‘type’: interruptEvent.forceType
});
}
};

const resultCode = await this.audioManager?.requestAudioFocus(focusRequest);
result.success(resultCode === audio.AudioFocusRequestResult.AUDIO_FOCUS_REQUEST_GRANTED);

}

private abandonAudioFocus(result: any) {
this.audioManager?.abandonAudioFocus();
result.success(true);
}
}
运行效果
频谱动画实时跟随音乐节奏

支持后台播放(鸿蒙6.0媒体会话自动适配)

可通过元服务卡片控制播放/暂停

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐