Flutter 音频处理¶
使用 TTS(文本转语音)将文字转为可播放的音频,支持多种音色选择。
功能预览¶

左侧导航选择 🔊 音频处理 → 输入文本 → 选择音色 → 生成并播放语音
目录¶
基础概念¶
文本转语音(TTS)API 将文本转换为音频文件(mp3 / wav / opus):
{
'model': 'tts-1',
'input': '欢迎使用 Flutter 教程',
'voice': 'alloy',
'response_format': 'mp3',
'speed': 1.0,
}
常用模型与音色:
| 模型 | 说明 | 可选音色 |
|---|---|---|
tts-1 / tts-1-hd | OpenAI 标准 TTS | alloy, echo, fable, onyx, nova, shimmer |
doubao-tts-pro-44100 | 字节豆包 TTS | 多种中文音色 |
fish-speech-v1.4 | 开源多音色 TTS | 多种语言 |
调用 TTS API¶
import 'package:path_provider/path_provider.dart';
import 'dart:io';
Future<String> textToSpeech(String text, {
String voice = 'alloy',
double speed = 1.0,
}) async {
final response = await http.post(
Uri.parse('${dotenv.env['APINEXUS_BASE_URL']!}/audio/speech'),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ${dotenv.env['APINEXUS_API_KEY']!}',
},
body: json.encode({
'model': 'tts-1',
'input': text,
'voice': voice,
'response_format': 'mp3',
'speed': speed,
}),
);
// 保存到本地文件
final dir = await getTemporaryDirectory();
final file = File('${dir.path}/speech_${DateTime.now().millisecondsSinceEpoch}.mp3');
await file.writeAsBytes(response.bodyBytes);
return file.path;
}
播放音频文件¶
使用 audioplayers 包播放:
import 'package:audioplayers/audioplayers.dart';
class AudioPlayerService {
final AudioPlayer _player = AudioPlayer();
bool _isPlaying = false;
Future<void> play(String filePath) async {
await _player.play(DeviceFileSource(filePath));
_isPlaying = true;
}
Future<void> pause() async {
await _player.pause();
_isPlaying = false;
}
Future<void> stop() async {
await _player.stop();
_isPlaying = false;
}
bool get isPlaying => _isPlaying;
Stream<Duration> get onPositionChanged => _player.onPositionChanged;
Stream<Duration?> get onDuration => _player.onDurationChanged;
void dispose() {
_player.dispose();
}
}
流式播放¶
如果 TTS API 支持流式响应,可以一边接收一边播放(chunked audio):
Future<List<int>> streamTTS(String text) async {
final request = http.Request(
'POST',
Uri.parse('${dotenv.env['APINEXUS_BASE_URL']!}/audio/speech'),
);
request.headers['Content-Type'] = 'application/json';
request.headers['Authorization'] =
'Bearer ${dotenv.env['APINEXUS_API_KEY']!}';
request.body = json.encode({
'model': 'tts-1',
'input': text,
'voice': 'alloy',
'response_format': 'mp3',
});
final response = await request.send();
final bytes = await response.stream.toBytes();
// 保存到临时文件
final dir = await getTemporaryDirectory();
final file = File('${dir.path}/stream_tts.mp3');
await file.writeAsBytes(bytes);
return bytes;
}
音色选择¶
不同模型支持不同的音色。以下是 OpenAI TTS 标准音色说明:
| 音色 | 说明 | 适用场景 |
|---|---|---|
alloy | 中性偏年轻男声 | 通用聊天、播报 |
echo | 成熟稳重男声 | 新闻、说明类内容 |
fable | 故事讲述风格 | 儿童故事、有声书 |
onyx | 深沉男声 | 广告旁白、电影预告片 |
nova | 年轻女声 | 播客、讲解 |
shimmer | 柔和女声 | 情感类内容 |
在 UI 中选择音色:
final voices = ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'];
String selected = 'alloy';
DropdownButton<String>(
value: selected,
items: voices.map((v) => DropdownMenuItem(value: v, child: Text(v))).toList(),
onChanged: (v) => setState(() => selected = v!),
);
完整示例¶
在 Flutter 应用中集成 TTS 播放:
class AudioPage extends StatefulWidget {
const AudioPage({super.key});
@override
State<AudioPage> createState() => _AudioPageState();
}
class _AudioPageState extends State<AudioPage> {
final _controller = TextEditingController(
text: '欢迎使用 Flutter 音频处理教程。本教程将教你如何将文字转换为语音。',
);
final _audio = AudioPlayerService();
String _voice = 'alloy';
double _speed = 1.0;
String _status = '';
Duration _position = Duration.zero;
Duration _duration = Duration.zero;
bool _loading = false;
@override
void initState() {
super.initState();
_audio.onPositionChanged.listen((d) => setState(() => _position = d));
_audio.onDurationChanged.listen((d) => setState(() => _duration = d));
}
@override
void dispose() {
_audio.dispose();
super.dispose();
}
Future<void> _generateAndPlay() async {
if (_controller.text.isEmpty || _loading) return;
setState(() {
_loading = true;
_status = '正在生成音频...';
});
try {
final filePath = await textToSpeech(
_controller.text,
voice: _voice,
speed: _speed,
);
setState(() => _status = '生成完成,开始播放');
await _audio.play(filePath);
} catch (e) {
setState(() => _status = '失败: $e');
} finally {
setState(() => _loading = false);
}
}
String _formatDuration(Duration d) {
final min = d.inMinutes;
final sec = d.inSeconds.remainder(60);
return '${min}:${sec.toString().padLeft(2, '0')}';
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('TTS 文本转语音')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
controller: _controller,
maxLines: 4,
decoration: const InputDecoration(
labelText: '输入要朗读的文字',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
Row(
children: [
const Text('音色:'),
const SizedBox(width: 8),
Expanded(
child: DropdownButton<String>(
isExpanded: true,
value: _voice,
items: ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer']
.map((v) => DropdownMenuItem(value: v, child: Text(v)))
.toList(),
onChanged: (v) => setState(() => _voice = v!),
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
const Text('速度:'),
Expanded(
child: Slider(
min: 0.25,
max: 4.0,
value: _speed,
onChanged: (v) => setState(() => _speed = v),
),
),
Text(_speed.toStringAsFixed(1) + 'x'),
],
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: _loading ? null : _generateAndPlay,
child: Text(_loading ? '生成中...' : '生成并播放'),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton(
onPressed: () => _audio.pause(),
child: const Text('暂停'),
),
),
],
),
const SizedBox(height: 16),
if (_duration.inSeconds > 0) ...[
Slider(
min: 0,
max: _duration.inSeconds.toDouble(),
value: _position.inSeconds.toDouble(),
onChanged: (_) {},
),
Text('${_formatDuration(_position)} / ${_formatDuration(_duration)}'),
],
const SizedBox(height: 12),
Text(_status, style: Theme.of(context).textTheme.bodySmall),
],
),
),
);
}
}