diff --git a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/MainActivity.kt b/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/MainActivity.kt index 499bbf3..73b6ada 100644 --- a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/MainActivity.kt +++ b/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/MainActivity.kt @@ -16,6 +16,8 @@ class MainActivity : FlutterActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Log.i("WowEnglish", "MainActivity onCreate") + + flutterEngine?.let { SingSoungMethodChannel(this, it) } } override fun onResume() { @@ -46,9 +48,4 @@ class MainActivity : FlutterActivity() { // 打开沉浸式 WindowCompat.setDecorFitsSystemWindows(window, false)*/ } - - override fun configureFlutterEngine(flutterEngine: FlutterEngine) { - super.configureFlutterEngine(flutterEngine) - SingSoungMethodChannel(this, flutterEngine) - } } diff --git a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/methodChannels/SingSoungMethodChannel.kt b/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/methodChannels/SingSoungMethodChannel.kt index f3df4fd..d68998c 100644 --- a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/methodChannels/SingSoungMethodChannel.kt +++ b/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/methodChannels/SingSoungMethodChannel.kt @@ -1,5 +1,7 @@ package com.kouyuxingqiu.wow_english.methodChannels +import android.util.Log +import com.kouyuxingqiu.wow_english.singsound.SingEngineHelper import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel @@ -30,10 +32,18 @@ class SingSoungMethodChannel(val activity: FlutterActivity, val flutterEngine: F ) methodChannel?.setMethodCallHandler { call, result -> when (call.method) { - "startRecord" -> { - val jsonStr = call.arguments as? String ?: return@setMethodCallHandler + "initVoiceSdk" -> { + SingEngineHelper.init(activity) + } + "startVoice" -> { + val paramMap = call.arguments as HashMap + Log.d("WQF", "SingSoungMethodChannel startVoice=${call.arguments.javaClass} paramMap=$paramMap") + paramMap["word"]?.let { SingEngineHelper.startRecord(it) } //do nothing } + "stopVoice" -> { + Log.d("WQF", "SingSoungMethodChannel stopVoice") + } else -> { result.notImplemented() } diff --git a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/SingEngineHelper.kt b/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/SingEngineHelper.kt index 41d7e62..6b59ccb 100644 --- a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/SingEngineHelper.kt +++ b/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/SingEngineHelper.kt @@ -18,7 +18,7 @@ import org.json.JSONObject import java.util.* -class SingEngineHelper private constructor() : +object SingEngineHelper : AudioErrorCallback, EvalReturnRequestIdCallback, OnRealTimeResultListener { private val TAG = "SingEngineManager" @@ -54,57 +54,59 @@ class SingEngineHelper private constructor() : mListeners = mutableListOf() if (mSingEngine == null) { mSingEngine = SingEngine.newInstance(context) - } - Thread { - try { - mSingEngine?.run { - // 设置测评结果监听器 - setListener(this@SingEngineHelper) - // 设置录音器初始化错误的回调 - setAudioErrorCallback(this@SingEngineHelper) - setEvalReturnRequestIdCallback(this@SingEngineHelper) + Thread { + try { + mSingEngine?.run { + // 设置测评结果监听器 + setListener(this@SingEngineHelper) + // 设置录音器初始化错误的回调 + setAudioErrorCallback(this@SingEngineHelper) + setEvalReturnRequestIdCallback(this@SingEngineHelper) // // 设置音频格式 // setAudioType(AudioTypeEnum.WAV) - // 设置引擎类型。引擎类型(在线CLOUD、 离线NATIVE、混合AUTO),默认使用在线引擎。 - setServerType(CoreProvideTypeEnum.CLOUD) - // 设置log日志级别 - setLogLevel(4) - // 禁用实时音量返回 - disableVolume() - // 设置录音音频路径 - wavPath = AiUtil.getFilesDir(context).path + "/userdata/sound_record/" - // 设置是否开启 VAD 功能 - setOpenVad(true, "vad.0.1.bin") - //setOpenVad(false, null); - // 设置 VAD 前置超时时间 - setFrontVadTime(3000) + // 设置引擎类型。引擎类型(在线CLOUD、 离线NATIVE、混合AUTO),默认使用在线引擎。 + setServerType(CoreProvideTypeEnum.CLOUD) + // 设置log日志级别 + setLogLevel(4) + // 禁用实时音量返回 + disableVolume() + // 设置录音音频路径 + wavPath = AiUtil.getFilesDir(context).path + "/userdata/sound_record/" + // 设置是否开启 VAD 功能 + setOpenVad(true, "vad.0.1.bin") + //setOpenVad(false, null); + // 设置 VAD 前置超时时间 + setFrontVadTime(3000) // setServerTimeout(10000) - // 开启错误日志保存到本地,发生错误时文件中会保存到android/data/包名/files/SSError.txt中 + // 开启错误日志保存到本地,发生错误时文件中会保存到android/data/包名/files/SSError.txt中 // setOpenWriteLog(true) - // 设置在线服务器地址和账号 - setServerAPI("wss://api.cloud.ssapi.cn") + // 设置在线服务器地址和账号 + setServerAPI("wss://api.cloud.ssapi.cn") // // 设置评测语言(针对离线评测) // setOffLineSource(OffLineSourceEnum.SOURCE_EN) - // 设置引擎初始化参数 - setNewCfg( - buildInitJson( - SingSoundConfig.APPKEY, - SingSoundConfig.SECERTKEY + // 设置引擎初始化参数 + setNewCfg( + buildInitJson( + SingSoundConfig.APPKEY, + SingSoundConfig.SECERTKEY + ) ) - ) - // 引擎初始化 - createEngine() - } + // 引擎初始化 + createEngine("1") - getSymbolsMap() - } catch (e: Exception) { - e.printStackTrace() - } - }.start() + Log.w(TAG, "createEngine") + } + + getSymbolsMap() + } catch (e: Exception) { + e.printStackTrace() + } + }.start() + } } // 开始语音评测 - fun startRecord(originText: String, @EvalTargetType evalTargetType: Int?) { + fun startRecord(originText: String, @EvalTargetType evalTargetType: Int? = EvalTargetType.SENTENCE) { try { val request = JSONObject() when (evalTargetType) { diff --git a/lib/generated/json/course_process_entity.g.dart b/lib/generated/json/course_process_entity.g.dart index 354a939..d91cd08 100644 --- a/lib/generated/json/course_process_entity.g.dart +++ b/lib/generated/json/course_process_entity.g.dart @@ -38,9 +38,9 @@ Map $CourseProcessEntityToJson(CourseProcessEntity entity) { CourseProcessReadings $CourseProcessReadingsFromJson(Map json) { final CourseProcessReadings courseProcessReadings = CourseProcessReadings(); - final String? auditUrl = jsonConvert.convert(json['auditUrl']); - if (auditUrl != null) { - courseProcessReadings.auditUrl = auditUrl; + final String? audioUrl = jsonConvert.convert(json['audioUrl']); + if (audioUrl != null) { + courseProcessReadings.audioUrl = audioUrl; } final int? courseLessonId = jsonConvert.convert(json['courseLessonId']); if (courseLessonId != null) { @@ -83,7 +83,7 @@ CourseProcessReadings $CourseProcessReadingsFromJson(Map json) Map $CourseProcessReadingsToJson(CourseProcessReadings entity) { final Map data = {}; - data['auditUrl'] = entity.auditUrl; + data['audioUrl'] = entity.audioUrl; data['courseLessonId'] = entity.courseLessonId; data['createTime'] = entity.createTime; data['deleted'] = entity.deleted; diff --git a/lib/models/course_process_entity.dart b/lib/models/course_process_entity.dart index d88e603..dacf237 100644 --- a/lib/models/course_process_entity.dart +++ b/lib/models/course_process_entity.dart @@ -24,7 +24,7 @@ class CourseProcessEntity { @JsonSerializable() class CourseProcessReadings { - String? auditUrl; + String? audioUrl; int? courseLessonId; String? createTime; String? deleted; diff --git a/lib/pages/practice/bloc/topic_picture_bloc.dart b/lib/pages/practice/bloc/topic_picture_bloc.dart index 0bc9ade..160eb98 100644 --- a/lib/pages/practice/bloc/topic_picture_bloc.dart +++ b/lib/pages/practice/bloc/topic_picture_bloc.dart @@ -142,7 +142,7 @@ class TopicPictureBloc extends Bloc { void _voiceXsTest(XSVoiceTestEvent event,Emitter emitter) async { EasyLoading.show(status: '录音中....'); methodChannel.invokeMethod( - 'starVoice', + 'startVoice', {'word':event.testWord,'type':event.type,'userId':event.userId.toString()} ); _isVoicing = true; diff --git a/lib/pages/reading/bloc/reading_bloc.dart b/lib/pages/reading/bloc/reading_bloc.dart index 79530f2..01f420e 100644 --- a/lib/pages/reading/bloc/reading_bloc.dart +++ b/lib/pages/reading/bloc/reading_bloc.dart @@ -1,12 +1,20 @@ +import 'package:audioplayers/audioplayers.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:wow_english/pages/reading/widgets/ReadingModeType.dart'; +import '../../../common/request/dao/listen_dao.dart'; +import '../../../common/request/exception.dart'; +import '../../../models/course_process_entity.dart'; +import '../../../utils/loading.dart'; + part 'reading_event.dart'; part 'reading_state.dart'; class ReadingPageBloc extends Bloc { - final PageController pageController; ///当前页索引 @@ -19,26 +27,68 @@ class ReadingPageBloc extends Bloc { ReadingModeType get currentMode => _currentMode; + CourseProcessEntity? _entity; + + CourseProcessEntity? get entity => _entity; + + ///正在评测 + bool _isRecording = false; + + bool get isRecording => _isRecording; + + late MethodChannel methodChannel; + + late AudioPlayer audioPlayer; + ReadingPageBloc(this.pageController) : super(ReadingPageInitial()) { on(_pageControllerChange); on(_selectItemLoad); // pageController.addListener(() { // _currentPage = pageController.page!.round(); // }); + on(_requestData); + on((event, emit) { + //音频播放器 + audioPlayer = AudioPlayer(); + audioPlayer.onPlayerStateChanged.listen((event) { + if (event == PlayerState.completed) { + if (kDebugMode) { + print('绘本播放完成'); + + } + } + }); + + methodChannel = const MethodChannel('sing_sound_method_channel'); + methodChannel.invokeMethod('initVoiceSdk',{}); + methodChannel.setMethodCallHandler((call) async { + if (call.method == 'voiceResult') {//评测结束 + // add(XSVoiceResultEvent(call.arguments)); + } + }); + }); + on(_playOriginalAudio); + on(_voiceXsTest); + on(_voiceXsResult); } @override Future close() { pageController.dispose(); + audioPlayer.release(); + audioPlayer.dispose(); return super.close(); } - void _pageControllerChange(CurrentPageIndexChangeEvent event, Emitter emitter) async { + void _pageControllerChange(CurrentPageIndexChangeEvent event, + Emitter emitter) async { _currentPage = event.pageIndex; + _playOriginVoice(null); emitter(CurrentPageIndexState()); } - void _selectItemLoad(CurrentModeChangeEvent event, Emitter emitter) async { + void _selectItemLoad( + CurrentModeChangeEvent event, Emitter emitter) async { if (_currentMode == ReadingModeType.auto) { _currentMode = ReadingModeType.manual; } else { @@ -46,4 +96,77 @@ class ReadingPageBloc extends Bloc { } emitter(CurrentModeState()); } + + ///请求数据 + void _requestData( + RequestDataEvent event, Emitter emitter) async { + try { + await loading(() async { + _entity = await ListenDao.process('1'); + print("reading page entity: ${_entity!.toJson()}"); + emitter(RequestDataState()); + }); + } catch (e) { + if (e is ApiException) { + EasyLoading.showToast(e.message ?? '请求失败,请检查网络连接'); + } + } + } + + void _playOriginalAudio(PlayOriginalAudioEvent event, Emitter emitter) async { + print("_playOriginalAudio"); + _playOriginVoice(event.url); + } + + /// 播放绘本原音 + void _playOriginVoice(String? audioUrl) async { + audioPlayer.stop(); + final readingData = currentPageData(); + if (readingData?.audioUrl != null) { + final urlStr = audioUrl ?? readingData?.audioUrl ?? ''; + if (urlStr.isNotEmpty) { + audioPlayer.play(UrlSource(urlStr)); + } + } + } + + int dataCount() { + // print("dataCount=${_entity?.readings?.length ?? 0}"); + return _entity?.readings?.length ?? 0; + } + + CourseProcessReadings? currentPageData() { + return _entity?.readings?[_currentPage]; + } + + ///先声测试 + void _voiceXsTest(XSVoiceTestEvent event, Emitter emitter) async { + startRecord(event.content); + emitter(XSVoiceTestState()); + } + + void startRecord(String content) async { + if (_isRecording == true) { + return; + } + EasyLoading.show(status: '录音中....'); + methodChannel.invokeMethod( + 'startVoice', + {'word':'how old are you','type':'0','userId':'1'} + ); + _isRecording = true; + } + + void _voiceXsResult(XSVoiceResultEvent event,Emitter emitter) async { + final Map args = event.message as Map; + final result = args['result'] as String; + if (result == '1') { + final overall = args['overall'].toString(); + EasyLoading.showToast('测评成功,分数是$overall',duration: const Duration(seconds: 10)); + } else { + EasyLoading.showToast('测评失败',duration: const Duration(seconds: 10)); + } + _isRecording = false; + emitter(XSVoiceTestState()); + } } diff --git a/lib/pages/reading/bloc/reading_event.dart b/lib/pages/reading/bloc/reading_event.dart index 2e71909..cbcd3ea 100644 --- a/lib/pages/reading/bloc/reading_event.dart +++ b/lib/pages/reading/bloc/reading_event.dart @@ -8,4 +8,33 @@ class CurrentPageIndexChangeEvent extends ReadingPageEvent { CurrentPageIndexChangeEvent(this.pageIndex); } -class CurrentModeChangeEvent extends ReadingPageEvent {} \ No newline at end of file +class CurrentModeChangeEvent extends ReadingPageEvent {} + +///请求接口获取数据 +class RequestDataEvent extends ReadingPageEvent {} + +///播放原音频 +class PlayOriginalAudioEvent extends ReadingPageEvent { + final String? url; + PlayOriginalAudioEvent(this.url); +} + +///初始化先声SDK +class XSVoiceInitEvent extends ReadingPageEvent { + final Map data; + XSVoiceInitEvent(this.data); +} + +///评测结果 +class XSVoiceResultEvent extends ReadingPageEvent { + final dynamic message; + XSVoiceResultEvent(this.message); +} + +///先声测试 +class XSVoiceTestEvent extends ReadingPageEvent { + final String content; + final String type; + final String userId; + XSVoiceTestEvent(this.content,this.type,this.userId); +} \ No newline at end of file diff --git a/lib/pages/reading/bloc/reading_state.dart b/lib/pages/reading/bloc/reading_state.dart index d3e0411..cb76e63 100644 --- a/lib/pages/reading/bloc/reading_state.dart +++ b/lib/pages/reading/bloc/reading_state.dart @@ -9,3 +9,7 @@ class CurrentPageIndexState extends ReadingPageState {} /// 手动or自动播放 class CurrentModeState extends ReadingPageState {} + +class RequestDataState extends ReadingPageState {} + +class XSVoiceTestState extends ReadingPageState {} diff --git a/lib/pages/reading/reading_page.dart b/lib/pages/reading/reading_page.dart index 4bf868c..1993f56 100644 --- a/lib/pages/reading/reading_page.dart +++ b/lib/pages/reading/reading_page.dart @@ -4,6 +4,8 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:wow_english/common/extension/string_extension.dart'; import 'package:wow_english/pages/reading/widgets/ReadingModeType.dart'; +import '../../common/core/user_util.dart'; +import '../../models/course_process_entity.dart'; import 'bloc/reading_bloc.dart'; class ReadingPage extends StatelessWidget { @@ -12,7 +14,7 @@ class ReadingPage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (_) => ReadingPageBloc(PageController()), + create: (_) => ReadingPageBloc(PageController())..add(RequestDataEvent()), child: _ReadingPage(), ); } @@ -22,7 +24,15 @@ class _ReadingPage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocListener( - listener: (context, state) {}, + listener: (context, state) { + if (state is RequestDataState) { + // context.read().add(CurrentPageIndexChangeEvent(0)); + print('reading RequestDataState=$state'); + + ///刷新页面 + context.read().add(CurrentPageIndexChangeEvent(0)); + } + }, child: _readingPageView(), ); } @@ -36,13 +46,13 @@ class _ReadingPage extends StatelessWidget { child: Stack( children: [ PageView.builder( - itemCount: 10, + itemCount: bloc.dataCount(), controller: bloc.pageController, onPageChanged: (int index) { bloc.add(CurrentPageIndexChangeEvent(index)); }, itemBuilder: (context, int index) { - return _readingPagerItem(); + return _readingPagerItem(bloc.entity!.readings![index]); }), Container( color: Colors.transparent, @@ -76,15 +86,14 @@ class _ReadingPage extends StatelessWidget { ), alignment: Alignment.center, child: Text( - '${bloc.currentPage}/10', - - ///todo 分母需要替换成数据数组长度 + '${bloc.currentPage}/${bloc.dataCount()}', style: TextStyle(fontSize: 20.sp, color: Colors.white), ), ), Padding( - padding: EdgeInsets.only(right: 15.w + ScreenUtil().bottomBarHeight), + padding: EdgeInsets.only( + right: 15.w + ScreenUtil().bottomBarHeight), child: GestureDetector( onTap: () { bloc.add(CurrentModeChangeEvent()); @@ -121,17 +130,26 @@ class _ReadingPage extends StatelessWidget { margin: EdgeInsets.symmetric(horizontal: 10.w), child: Row( children: [ - Image.asset( - 'voice'.assetPng, - height: 40.h, - width: 45.w, + GestureDetector( + onTap: () { + if (bloc.isRecording) { + return; + } + print("voice tap"); + bloc.add(PlayOriginalAudioEvent(null)); + }, + child: Image.asset( + 'voice'.assetPng, + height: 40.h, + width: 45.w, + ), ), SizedBox( width: 10.w, ), Expanded( child: Text( - "HelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorld", + bloc.currentPageData()?.word ?? '', style: TextStyle( color: const Color(0xFF333333), fontSize: 21.sp), maxLines: 2, @@ -140,11 +158,18 @@ class _ReadingPage extends StatelessWidget { SizedBox( width: 10.w, ), - Image.asset( - 'micro_phone'.assetPng, - height: 47.h, - width: 47.w, - ), + GestureDetector( + onTap: () { + if (bloc.isRecording) { + return; + } + bloc.add(XSVoiceTestEvent(bloc.currentPageData()?.word??'', '0',UserUtil.getUser()!.id.toString())); + }, + child: Image.asset( + 'micro_phone'.assetPng, + height: 47.h, + width: 47.w, + )), SizedBox( width: 10.w, ), @@ -169,14 +194,12 @@ class _ReadingPage extends StatelessWidget { ); }); - Widget _readingPagerItem() => + Widget _readingPagerItem(CourseProcessReadings readings) => BlocBuilder(builder: (context, state) { return Stack( children: [ - Image.network( - 'https://img.liblibai.com/web/648331d5a2cb5.png?image_process=format,webp&x-oss-process=image/resize,w_2980,m_lfit/format,webp', - height: double.infinity, - width: double.infinity), + Image.network(readings.picUrl ?? '', + height: double.infinity, width: double.infinity), ], ); });