import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:path_provider/path_provider.dart'; import 'package:just_audio/just_audio.dart'; import 'package:jl_opus/jl_opus.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:share_plus/share_plus.dart'; // 添加分享插件导入 class OpusTestController extends GetxController { // 选中的文件 final selectedFiles = [].obs; // 扫描到的外部存储文件 final externalFiles = [].obs; // 是否携带协议头 final hasHeader = false.obs; // 数据包长度 final packetLength = '80'.obs; // 采样率选项 final sampleRateOptions = [8000, 16000, 24000, 32000, 44100, 48000]; final selectedSampleRate = 16000.obs; // 通道数量 final isMono = true.obs; // 播放状态 final isPlaying = false.obs; final isDecoding = false.obs; final statusMessage = ''.obs; // 播放器 final player = AudioPlayer(); // 临时文件路径 String? tempPcmPath; // 杰理OPUS解码器 late JlOpus jlOpus; StreamSubscription? _eventSubscription; @override void onInit() { super.onInit(); requestPermissions(); _initOpusDecoder(); } @override void onClose() { player.dispose(); _eventSubscription?.cancel(); jlOpus.dispose(); super.onClose(); } // 初始化OPUS解码器 Future _initOpusDecoder() async { jlOpus = JlOpus(); // 监听解码器事件 _eventSubscription = jlOpus.eventStream.listen((event) { switch (event.event) { case 'onStart': statusMessage.value = '开始${event.type == "file" ? "文件" : "流"}解码...'; break; case 'onComplete': if (event.type == 'file') { statusMessage.value = '解码完成: ${event.filePath}'; } else { statusMessage.value = '流解码完成'; } break; case 'onError': statusMessage.value = '解码错误: [${event.errorCode}] ${event.errorMessage}'; isDecoding.value = false; break; } }); // 初始化OPUS解码器 final initialized = await jlOpus.initOpusDecoder(); if (!initialized) { statusMessage.value = 'OPUS解码器初始化失败'; } } // 请求必要权限 Future requestPermissions() async { if (Platform.isAndroid) { await Permission.storage.request(); } } // 加载外部存储文件 Future loadExternalFiles() async { try { externalFiles.clear(); statusMessage.value = '正在扫描存储...'; print('正在扫描存储...'); List directories = []; if (Platform.isAndroid) { // Android: 使用外部存储目录 final androidDirs = await getExternalStorageDirectories(); if (androidDirs != null && androidDirs.isNotEmpty) { directories.addAll(androidDirs); } } else if (Platform.isIOS) { // iOS: 使用Documents目录 final iosDir = await getApplicationDocumentsDirectory(); directories.add(iosDir); } if (directories.isEmpty) { statusMessage.value = '未找到存储目录'; return; } print('存储目录: $directories'); // 遍历所有存储目录 for (final directory in directories) { final dirPath = directory.path; final dir = Directory(dirPath); // 检查目录是否存在 if (await dir.exists()) { // 递归列出所有文件 final files = await dir.list(recursive: true).toList(); for (var entity in files) { if (entity is File) { final path = entity.path.toLowerCase(); if (path.endsWith('.opus') || path.endsWith('.txt')) { final fileSize = await entity.length(); externalFiles.add(PlatformFile( name: entity.path.split('/').last, path: entity.path, size: fileSize, )); } } } } } statusMessage.value = '扫描完成,找到 ${externalFiles.length} 个Opus文件'; } catch (e) { statusMessage.value = '扫描失败: $e'; } } // 选择文件 Future pickFiles() async { try { final result = await FilePicker.platform.pickFiles( type: FileType.custom, allowedExtensions: ['opus'], allowMultiple: false, ); if (result != null && result.files.isNotEmpty) { selectedFiles.clear(); selectedFiles.add(result.files.first); statusMessage.value = '已选择文件:${result.files.first.name}'; } } catch (e) { statusMessage.value = '选择文件失败: $e'; } } // 从外部文件列表中选择文件 void selectExternalFile(PlatformFile file) { selectedFiles.clear(); selectedFiles.add(file); statusMessage.value = '已选择文件: ${file.name}'; } // 切换通道模式 void toggleChannel(bool isSingleChannel) { isMono.value = isSingleChannel; } // 设置采样率 void setSampleRate(int rate) { selectedSampleRate.value = rate; } // 设置数据包长度 void setPacketLength(String length) { packetLength.value = length; } // 切换是否携带协议头 void toggleHeader(bool value) { hasHeader.value = value; } // 播放音频 Future playAudio() async { if (isPlaying.value) { await player.stop(); isPlaying.value = false; statusMessage.value = '已停止播放'; return; } if (selectedFiles.isEmpty) { statusMessage.value = '请先选择Opus文件'; return; } try { isDecoding.value = true; statusMessage.value = '正在解码...'; // 使用杰理OPUS库进行解码 final inPath = selectedFiles.first.path!; // 创建WAV输出文件路径 final tempDir = await getTemporaryDirectory(); final wavFileName = 'decoded_opus_${DateTime.now().millisecondsSinceEpoch}.wav'; final outPath = '${tempDir.path}/$wavFileName'; // 创建解码参数 final option = OpusOption( hasHeader: hasHeader.value, channel: isMono.value ? 1 : 2, sampleRate: selectedSampleRate.value, packetSize: int.tryParse(packetLength.value) ?? 40, ); // 解码文件 final pcmPath = await jlOpus.decodeOpusFile(inPath, outPath, option); if (pcmPath == null) { statusMessage.value = '解码失败'; isDecoding.value = false; return; } print("pcmPath: $pcmPath"); tempPcmPath = pcmPath; // 将PCM文件转换为WAV文件 final wavFile = await _convertPcmToWav( pcmPath, '${tempDir.path}/$wavFileName', selectedSampleRate.value, isMono.value ? 1 : 2); statusMessage.value = '解码完成,准备播放'; // 播放WAV文件 await player.setFilePath(wavFile.path); await player.play(); isPlaying.value = true; statusMessage.value = '正在播放解码后的音频'; // 监听播放完成 player.playerStateStream.listen((state) { if (state.processingState == ProcessingState.completed) { isPlaying.value = false; statusMessage.value = '播放完成'; } }); } catch (e) { statusMessage.value = '解码或播放失败: $e'; } finally { isDecoding.value = false; } } // 将PCM转换为WAV格式 Future _convertPcmToWav( String pcmPath, String wavPath, int sampleRate, int channels) async { final pcmFile = File(pcmPath); final pcmData = await pcmFile.readAsBytes(); // 创建WAV文件 (44字节头部 + PCM数据) final wavSize = 44 + pcmData.length; final wavData = ByteData(wavSize); // 写入WAV头部 _writeWavHeader(wavData, pcmData.length, sampleRate, channels); // 写入PCM数据 for (int i = 0; i < pcmData.length; i++) { wavData.setUint8(44 + i, pcmData[i]); } // 保存为WAV文件 final wavFile = File(wavPath); await wavFile.writeAsBytes(wavData.buffer.asUint8List()); return wavFile; } // 写入WAV头部到ByteData void _writeWavHeader( ByteData data, int pcmLength, int sampleRate, int channels) { final bytesPerSample = 2; // 16位PCM final byteRate = sampleRate * channels * bytesPerSample; final blockAlign = channels * bytesPerSample; // "RIFF"标识 data.setUint8(0, 0x52); // 'R' data.setUint8(1, 0x49); // 'I' data.setUint8(2, 0x46); // 'F' data.setUint8(3, 0x46); // 'F' // 文件大小 (减去8个字节的RIFF头部和大小字段) data.setUint32(4, 36 + pcmLength, Endian.little); // "WAVE"标识 data.setUint8(8, 0x57); // 'W' data.setUint8(9, 0x41); // 'A' data.setUint8(10, 0x56); // 'V' data.setUint8(11, 0x45); // 'E' // "fmt "子块 data.setUint8(12, 0x66); // 'f' data.setUint8(13, 0x6D); // 'm' data.setUint8(14, 0x74); // 't' data.setUint8(15, 0x20); // ' ' // 子块大小 (16 for PCM) data.setUint32(16, 16, Endian.little); // 音频格式 (1 表示PCM) data.setUint16(20, 1, Endian.little); // 通道数量 data.setUint16(22, channels, Endian.little); // 采样率 data.setUint32(24, sampleRate, Endian.little); // 字节率 = 采样率 * 通道数 * 每样本字节数 data.setUint32(28, byteRate, Endian.little); // 块对齐 = 通道数 * 每样本字节数 data.setUint16(32, blockAlign, Endian.little); // 每个样本位数 data.setUint16(34, bytesPerSample * 8, Endian.little); // "data"子块 data.setUint8(36, 0x64); // 'd' data.setUint8(37, 0x61); // 'a' data.setUint8(38, 0x74); // 't' data.setUint8(39, 0x61); // 'a' // 数据大小 data.setUint32(40, pcmLength, Endian.little); } /// 删除外部文件 Future deleteExternalFile(PlatformFile file) async { try { final fileToDelete = File(file.path!); if (await fileToDelete.exists()) { await fileToDelete.delete(); externalFiles.removeWhere((f) => f.path == file.path); // 如果删除的是当前选中的文件,清空选择 if (selectedFiles.isNotEmpty && selectedFiles.first.path == file.path) { selectedFiles.clear(); } statusMessage.value = '文件已删除: ${file.name}'; } else { statusMessage.value = '文件不存在: ${file.name}'; } } catch (e) { statusMessage.value = '删除文件失败: $e'; } } /// 分享外部文件 Future shareExternalFile(PlatformFile file) async { try { if (file.path != null) { final fileToShare = File(file.path!); if (await fileToShare.exists()) { await Share.shareXFiles( [XFile(file.path!)], text: '分享Opus音频文件: ${file.name}', ); statusMessage.value = '正在分享文件: ${file.name}'; } else { statusMessage.value = '文件不存在: ${file.name}'; } } } catch (e) { statusMessage.value = '分享文件失败: $e'; } } /// 显示文件操作底部弹窗 void showFileActionSheet(BuildContext context, PlatformFile file) { showModalBottomSheet( context: context, backgroundColor: Get.isDarkMode ? Colors.grey[900] : Colors.white, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), builder: (context) => Container( padding: const EdgeInsets.symmetric(vertical: 20), child: Column( mainAxisSize: MainAxisSize.min, children: [ // 文件信息 Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ Icon( Icons.audiotrack, color: Get.isDarkMode ? Colors.white70 : Colors.black87, ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( file.name, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, color: Get.isDarkMode ? Colors.white : Colors.black, ), overflow: TextOverflow.ellipsis, ), Text( '${(file.size / 1024).toStringAsFixed(2)} KB', style: TextStyle( fontSize: 14, color: Get.isDarkMode ? Colors.white60 : Colors.black54, ), ), ], ), ), ], ), ), const Divider(), // 分享选项 ListTile( leading: const Icon(Icons.share, color: Colors.blue), title: const Text('分享文件'), onTap: () { Navigator.pop(context); shareExternalFile(file); }, ), // 删除选项 ListTile( leading: const Icon(Icons.delete, color: Colors.red), title: const Text('删除文件', style: TextStyle(color: Colors.red)), onTap: () { Navigator.pop(context); _showDeleteConfirmDialog(context, file); }, ), // 取消选项 ListTile( leading: const Icon(Icons.cancel), title: const Text('取消'), onTap: () => Navigator.pop(context), ), ], ), ), ); } /// 显示删除确认对话框 void _showDeleteConfirmDialog(BuildContext context, PlatformFile file) { showDialog( context: context, builder: (context) => AlertDialog( backgroundColor: Get.isDarkMode ? Colors.grey[900] : Colors.white, title: Text( '确认删除', style: TextStyle( color: Get.isDarkMode ? Colors.white : Colors.black, ), ), content: Text( '确定要删除文件 "${file.name}" 吗?\n此操作不可撤销。', style: TextStyle( color: Get.isDarkMode ? Colors.white70 : Colors.black87, ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('取消'), ), TextButton( onPressed: () { Navigator.pop(context); deleteExternalFile(file); }, style: TextButton.styleFrom(foregroundColor: Colors.red), child: const Text('删除'), ), ], ), ); } } // PCM音频源 class PCMAudioSource extends StreamAudioSource { final Uint8List pcmData; final int sampleRate; final int channels; PCMAudioSource({ required this.pcmData, this.sampleRate = 16000, this.channels = 1, }); @override Future request([int? start, int? end]) async { start = start ?? 0; end = end ?? pcmData.length; // 转换PCM到WAV格式 final wavHeader = _createWavHeader(pcmData.length, sampleRate, channels); final wavData = Uint8List(wavHeader.length + pcmData.length); wavData.setRange(0, wavHeader.length, wavHeader); wavData.setRange( wavHeader.length, wavHeader.length + pcmData.length, pcmData); // 调整请求范围来适应WAV头部 int adjustedStart = start; int adjustedEnd = end; if (start < wavHeader.length) { adjustedStart = start; } else { adjustedStart = start + wavHeader.length; } if (end <= wavHeader.length) { adjustedEnd = end; } else { adjustedEnd = end + wavHeader.length; adjustedEnd = adjustedEnd > wavData.length ? wavData.length : adjustedEnd; } return StreamAudioResponse( sourceLength: wavData.length, contentLength: adjustedEnd - adjustedStart, offset: adjustedStart, stream: Stream.value(wavData.sublist(adjustedStart, adjustedEnd)), contentType: 'audio/wav', ); } // 创建WAV头部 Uint8List _createWavHeader(int dataLength, int sampleRate, int channels) { final bytesPerSample = 2; // 16位PCM final byteRate = sampleRate * channels * bytesPerSample; final blockAlign = channels * bytesPerSample; final header = ByteData(44); // WAV头部固定44字节 // "RIFF"标识 header.setUint8(0, 0x52); // 'R' header.setUint8(1, 0x49); // 'I' header.setUint8(2, 0x46); // 'F' header.setUint8(3, 0x46); // 'F' // 文件大小 (减去8个字节的RIFF头部和大小字段) header.setUint32(4, 36 + dataLength, Endian.little); // "WAVE"标识 header.setUint8(8, 0x57); // 'W' header.setUint8(9, 0x41); // 'A' header.setUint8(10, 0x56); // 'V' header.setUint8(11, 0x45); // 'E' // "fmt "子块 header.setUint8(12, 0x66); // 'f' header.setUint8(13, 0x6D); // 'm' header.setUint8(14, 0x74); // 't' header.setUint8(15, 0x20); // ' ' // 子块大小 (16 for PCM) header.setUint32(16, 16, Endian.little); // 音频格式 (1 表示PCM) header.setUint16(20, 1, Endian.little); // 通道数量 header.setUint16(22, channels, Endian.little); // 采样率 header.setUint32(24, sampleRate, Endian.little); // 字节率 = 采样率 * 通道数 * 每样本字节数 header.setUint32(28, byteRate, Endian.little); // 块对齐 = 通道数 * 每样本字节数 header.setUint16(32, blockAlign, Endian.little); // 每个样本位数 header.setUint16(34, bytesPerSample * 8, Endian.little); // "data"子块 header.setUint8(36, 0x64); // 'd' header.setUint8(37, 0x61); // 'a' header.setUint8(38, 0x74); // 't' header.setUint8(39, 0x61); // 'a' // 数据大小 header.setUint32(40, dataLength, Endian.little); return header.buffer.asUint8List(); } }