You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
277 lines
7.8 KiB
277 lines
7.8 KiB
import 'dart:typed_data';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:opus/opus.dart';
|
|
|
|
void main() {
|
|
runApp(const OpusExampleApp());
|
|
}
|
|
|
|
class OpusExampleApp extends StatelessWidget {
|
|
const OpusExampleApp({Key? key}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return MaterialApp(
|
|
title: 'Opus Plugin Demo',
|
|
theme: ThemeData(
|
|
primarySwatch: Colors.blue,
|
|
),
|
|
home: const OpusExamplePage(),
|
|
);
|
|
}
|
|
}
|
|
|
|
class OpusExamplePage extends StatefulWidget {
|
|
const OpusExamplePage({Key? key}) : super(key: key);
|
|
|
|
@override
|
|
_OpusExamplePageState createState() => _OpusExamplePageState();
|
|
}
|
|
|
|
class _OpusExamplePageState extends State<OpusExamplePage> {
|
|
// 状态变量
|
|
bool _encoderInitialized = false;
|
|
bool _decoderInitialized = false;
|
|
String _statusMessage = '等待初始化...';
|
|
|
|
// 模拟PCM数据
|
|
Uint8List? _samplePcmData;
|
|
Uint8List? _encodedData;
|
|
Uint8List? _decodedData;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_initOpus();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_disposeOpus();
|
|
super.dispose();
|
|
}
|
|
|
|
// 初始化Opus编解码器
|
|
Future<void> _initOpus() async {
|
|
setState(() {
|
|
_statusMessage = '正在初始化编解码器...';
|
|
});
|
|
|
|
// 初始化编码器
|
|
final encoderResult = await Opus.createEncoder(
|
|
sampleRate: 48000,
|
|
channels: 2,
|
|
application: OpusApplicationType.audio,
|
|
complexity: 10,
|
|
frameSize: 960,
|
|
);
|
|
|
|
// 初始化解码器
|
|
final decoderResult = await Opus.createDecoder(
|
|
sampleRate: 48000,
|
|
channels: 2,
|
|
);
|
|
|
|
setState(() {
|
|
_encoderInitialized = encoderResult;
|
|
_decoderInitialized = decoderResult;
|
|
_statusMessage = _encoderInitialized && _decoderInitialized
|
|
? '编解码器初始化成功'
|
|
: '编解码器初始化失败';
|
|
});
|
|
|
|
// 创建模拟PCM数据用于测试
|
|
_createSamplePcmData();
|
|
}
|
|
|
|
// 创建模拟PCM数据(正弦波)
|
|
void _createSamplePcmData() {
|
|
// 创建一个简单的正弦波
|
|
const int sampleRate = 48000;
|
|
const int channels = 2;
|
|
const int frequency = 440; // 440Hz - A4音符
|
|
const int durationMs = 20; // 20ms,按照常规frameSize=960
|
|
const int frameSize = 960; // 对应20ms@48kHz
|
|
|
|
// 创建PCM数据缓冲区,16位有符号整数,每采样2字节,双声道
|
|
final int samplesToGenerate = (sampleRate * durationMs / 1000).round();
|
|
final int bufferSize = samplesToGenerate * channels * 2; // 2字节/样本
|
|
|
|
final byteBuffer = ByteData(bufferSize);
|
|
|
|
// 生成正弦波
|
|
for (int i = 0; i < samplesToGenerate; i++) {
|
|
// 计算当前时间点
|
|
final double t = i / sampleRate;
|
|
// 计算振幅值 (-1.0 to 1.0)
|
|
final double amplitude = sin(2 * 3.14159265359 * frequency * t);
|
|
// 缩放到16位有符号整数范围 (-32768 to 32767)
|
|
final int sample = (amplitude * 32767).round();
|
|
|
|
// 为所有声道写入相同的值
|
|
for (int channel = 0; channel < channels; channel++) {
|
|
final int offset = (i * channels + channel) * 2;
|
|
byteBuffer.setInt16(offset, sample, Endian.little);
|
|
}
|
|
}
|
|
|
|
_samplePcmData = Uint8List.fromList(
|
|
List.generate(bufferSize, (index) => byteBuffer.getUint8(index))
|
|
);
|
|
|
|
setState(() {
|
|
_statusMessage = '已创建${_samplePcmData!.length}字节测试PCM数据';
|
|
});
|
|
}
|
|
|
|
// 编码PCM数据
|
|
Future<void> _encodeData() async {
|
|
if (!_encoderInitialized || _samplePcmData == null) {
|
|
setState(() {
|
|
_statusMessage = '编码器未初始化或没有PCM数据';
|
|
});
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
_statusMessage = '正在编码...';
|
|
});
|
|
|
|
final encodedData = await Opus.encodeData(_samplePcmData!);
|
|
|
|
setState(() {
|
|
_encodedData = encodedData;
|
|
_statusMessage = encodedData != null
|
|
? '编码成功:${_samplePcmData!.length}字节PCM -> ${encodedData.length}字节Opus'
|
|
: '编码失败';
|
|
});
|
|
}
|
|
|
|
// 解码Opus数据
|
|
Future<void> _decodeData() async {
|
|
if (!_decoderInitialized || _encodedData == null) {
|
|
setState(() {
|
|
_statusMessage = '解码器未初始化或没有编码数据';
|
|
});
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
_statusMessage = '正在解码...';
|
|
});
|
|
|
|
final decodedData = await Opus.decodeData(_encodedData!, frameSize: 960);
|
|
|
|
setState(() {
|
|
_decodedData = decodedData;
|
|
_statusMessage = decodedData != null
|
|
? '解码成功:${_encodedData!.length}字节Opus -> ${decodedData.length}字节PCM'
|
|
: '解码失败';
|
|
});
|
|
}
|
|
|
|
// 释放资源
|
|
Future<void> _disposeOpus() async {
|
|
if (_encoderInitialized) {
|
|
await Opus.disposeEncoder();
|
|
}
|
|
if (_decoderInitialized) {
|
|
await Opus.disposeDecoder();
|
|
}
|
|
}
|
|
|
|
// 正弦波函数
|
|
double sin(double x) {
|
|
// 简单泰勒展开实现
|
|
double result = 0;
|
|
double term = x;
|
|
for (int i = 1; i <= 7; i += 2) {
|
|
result += term;
|
|
term = -term * x * x / ((i + 1) * (i + 2));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('Opus编解码示例'),
|
|
),
|
|
body: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
children: [
|
|
Text(
|
|
'状态:$_statusMessage',
|
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text('编码器:${_encoderInitialized ? "已初始化" : "未初始化"}'),
|
|
Text('解码器:${_decoderInitialized ? "已初始化" : "未初始化"}'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
ElevatedButton(
|
|
onPressed: _initOpus,
|
|
child: const Text('初始化编解码器'),
|
|
),
|
|
const SizedBox(height: 8),
|
|
ElevatedButton(
|
|
onPressed: _encodeData,
|
|
child: const Text('编码测试数据'),
|
|
),
|
|
const SizedBox(height: 8),
|
|
ElevatedButton(
|
|
onPressed: _decodeData,
|
|
child: const Text('解码测试数据'),
|
|
),
|
|
const SizedBox(height: 16),
|
|
if (_samplePcmData != null)
|
|
_buildDataInfoCard('源PCM数据', _samplePcmData!),
|
|
const SizedBox(height: 8),
|
|
if (_encodedData != null)
|
|
_buildDataInfoCard('编码后的Opus数据', _encodedData!),
|
|
const SizedBox(height: 8),
|
|
if (_decodedData != null)
|
|
_buildDataInfoCard('解码后的PCM数据', _decodedData!),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildDataInfoCard(String title, Uint8List data) {
|
|
return Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(12.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text('大小: ${data.length} 字节'),
|
|
const SizedBox(height: 4),
|
|
Text('前16字节: ${_formatBytes(data)}'),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
String _formatBytes(Uint8List data) {
|
|
final length = data.length > 16 ? 16 : data.length;
|
|
return data.sublist(0, length).map((e) => e.toRadixString(16).padLeft(2, '0')).join(' ');
|
|
}
|
|
}
|