Commit 53e9e6db47d8f13736fd21d0c7007ec822bd5615
1 parent
08a0f5a8
feat:绘本语音评测逻辑
Showing
14 changed files
with
279 additions
and
69 deletions
android/app/src/main/assets/vad.0.1.bin
100755 → 100644
No preview for this file type
android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/methodChannels/SingSoungMethodChannel.kt
| @@ -2,9 +2,13 @@ package com.kouyuxingqiu.wow_english.methodChannels | @@ -2,9 +2,13 @@ package com.kouyuxingqiu.wow_english.methodChannels | ||
| 2 | 2 | ||
| 3 | import android.util.Log | 3 | import android.util.Log |
| 4 | import com.kouyuxingqiu.wow_english.singsound.SingEngineHelper | 4 | import com.kouyuxingqiu.wow_english.singsound.SingEngineHelper |
| 5 | +import com.kouyuxingqiu.wow_english.singsound.SingEngineHelper.init | ||
| 6 | +import com.kouyuxingqiu.wow_english.singsound.SingEngineLifecycles | ||
| 7 | +import com.kouyuxingqiu.wow_english.util.GlobalHandler | ||
| 5 | import io.flutter.embedding.android.FlutterActivity | 8 | import io.flutter.embedding.android.FlutterActivity |
| 6 | import io.flutter.embedding.engine.FlutterEngine | 9 | import io.flutter.embedding.engine.FlutterEngine |
| 7 | import io.flutter.plugin.common.MethodChannel | 10 | import io.flutter.plugin.common.MethodChannel |
| 11 | +import org.json.JSONObject | ||
| 8 | import java.lang.ref.WeakReference | 12 | import java.lang.ref.WeakReference |
| 9 | 13 | ||
| 10 | /** | 14 | /** |
| @@ -12,13 +16,13 @@ import java.lang.ref.WeakReference | @@ -12,13 +16,13 @@ import java.lang.ref.WeakReference | ||
| 12 | * @date: 2023/6/27 00:32 | 16 | * @date: 2023/6/27 00:32 |
| 13 | * @description: | 17 | * @description: |
| 14 | */ | 18 | */ |
| 15 | -class SingSoungMethodChannel(val activity: FlutterActivity, val flutterEngine: FlutterEngine) { | 19 | +class SingSoungMethodChannel(activity: FlutterActivity, flutterEngine: FlutterEngine): SingEngineLifecycles.OnSingEngineAdapter() { |
| 16 | private var methodChannel: MethodChannel? = null | 20 | private var methodChannel: MethodChannel? = null |
| 17 | 21 | ||
| 18 | companion object { | 22 | companion object { |
| 19 | var channel: WeakReference<SingSoungMethodChannel>? = null | 23 | var channel: WeakReference<SingSoungMethodChannel>? = null |
| 20 | 24 | ||
| 21 | - fun invokeMethod(method: String, arguments: Any) { | 25 | + fun invokeMethod(method: String, arguments: Any?) { |
| 22 | channel?.get()?.methodChannel?.invokeMethod(method, arguments) | 26 | channel?.get()?.methodChannel?.invokeMethod(method, arguments) |
| 23 | } | 27 | } |
| 24 | } | 28 | } |
| @@ -30,10 +34,11 @@ class SingSoungMethodChannel(val activity: FlutterActivity, val flutterEngine: F | @@ -30,10 +34,11 @@ class SingSoungMethodChannel(val activity: FlutterActivity, val flutterEngine: F | ||
| 30 | flutterEngine.dartExecutor.binaryMessenger, | 34 | flutterEngine.dartExecutor.binaryMessenger, |
| 31 | "wow_english/sing_sound_method_channel" | 35 | "wow_english/sing_sound_method_channel" |
| 32 | ) | 36 | ) |
| 37 | + init(activity) | ||
| 33 | methodChannel?.setMethodCallHandler { call, result -> | 38 | methodChannel?.setMethodCallHandler { call, result -> |
| 34 | when (call.method) { | 39 | when (call.method) { |
| 35 | "initVoiceSdk" -> { | 40 | "initVoiceSdk" -> { |
| 36 | - SingEngineHelper.init(activity) | 41 | + |
| 37 | } | 42 | } |
| 38 | "startVoice" -> { | 43 | "startVoice" -> { |
| 39 | val paramMap = call.arguments as HashMap<String, String> | 44 | val paramMap = call.arguments as HashMap<String, String> |
| @@ -43,6 +48,7 @@ class SingSoungMethodChannel(val activity: FlutterActivity, val flutterEngine: F | @@ -43,6 +48,7 @@ class SingSoungMethodChannel(val activity: FlutterActivity, val flutterEngine: F | ||
| 43 | } | 48 | } |
| 44 | "stopVoice" -> { | 49 | "stopVoice" -> { |
| 45 | Log.d("WQF", "SingSoungMethodChannel stopVoice") | 50 | Log.d("WQF", "SingSoungMethodChannel stopVoice") |
| 51 | + SingEngineHelper.stopRecord() | ||
| 46 | } | 52 | } |
| 47 | else -> { | 53 | else -> { |
| 48 | result.notImplemented() | 54 | result.notImplemented() |
| @@ -51,5 +57,26 @@ class SingSoungMethodChannel(val activity: FlutterActivity, val flutterEngine: F | @@ -51,5 +57,26 @@ class SingSoungMethodChannel(val activity: FlutterActivity, val flutterEngine: F | ||
| 51 | 57 | ||
| 52 | } | 58 | } |
| 53 | channel = WeakReference(this) | 59 | channel = WeakReference(this) |
| 60 | + | ||
| 61 | + SingEngineHelper.addOnResultListener(this) | ||
| 62 | + } | ||
| 63 | + | ||
| 64 | + override fun onResult(jsonObject: JSONObject, evalType: Int?) { | ||
| 65 | + //先声回调在子线程,需要切换到主线程 | ||
| 66 | + GlobalHandler.runOnMainThread { | ||
| 67 | + invokeMethod("voiceResult", jsonObject.toString()) | ||
| 68 | + } | ||
| 69 | + } | ||
| 70 | + | ||
| 71 | + override fun onRecordBegin() { | ||
| 72 | + GlobalHandler.runOnMainThread { | ||
| 73 | + invokeMethod("voiceStart", null) | ||
| 74 | + } | ||
| 75 | + } | ||
| 76 | + | ||
| 77 | + override fun onRecordStop() { | ||
| 78 | + GlobalHandler.runOnMainThread { | ||
| 79 | + invokeMethod("voiceEnd", null) | ||
| 80 | + } | ||
| 54 | } | 81 | } |
| 55 | } | 82 | } |
| 56 | \ No newline at end of file | 83 | \ No newline at end of file |
android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/SingEngineHelper.kt
| @@ -73,7 +73,7 @@ object SingEngineHelper : | @@ -73,7 +73,7 @@ object SingEngineHelper : | ||
| 73 | // 设置录音音频路径 | 73 | // 设置录音音频路径 |
| 74 | wavPath = AiUtil.getFilesDir(context).path + "/userdata/sound_record/" | 74 | wavPath = AiUtil.getFilesDir(context).path + "/userdata/sound_record/" |
| 75 | // 设置是否开启 VAD 功能 | 75 | // 设置是否开启 VAD 功能 |
| 76 | - setOpenVad(true, "vad.0.1.bin") | 76 | +// setOpenVad(true, "vad.0.1.bin") |
| 77 | //setOpenVad(false, null); | 77 | //setOpenVad(false, null); |
| 78 | // 设置 VAD 前置超时时间 | 78 | // 设置 VAD 前置超时时间 |
| 79 | setFrontVadTime(3000) | 79 | setFrontVadTime(3000) |
| @@ -383,6 +383,7 @@ object SingEngineHelper : | @@ -383,6 +383,7 @@ object SingEngineHelper : | ||
| 383 | } | 383 | } |
| 384 | 384 | ||
| 385 | fun addOnResultListener(listener: SingEngineLifecycles) { | 385 | fun addOnResultListener(listener: SingEngineLifecycles) { |
| 386 | + Log.i(TAG, "addOnResultListener") | ||
| 386 | if (mListeners?.contains(listener) == true) { | 387 | if (mListeners?.contains(listener) == true) { |
| 387 | return | 388 | return |
| 388 | } | 389 | } |
| @@ -390,6 +391,7 @@ object SingEngineHelper : | @@ -390,6 +391,7 @@ object SingEngineHelper : | ||
| 390 | } | 391 | } |
| 391 | 392 | ||
| 392 | fun removeOnResultListener(listener: SingEngineLifecycles) { | 393 | fun removeOnResultListener(listener: SingEngineLifecycles) { |
| 394 | + Log.i(TAG, "removeOnResultListener") | ||
| 393 | if (mListeners?.contains(listener) == true) { | 395 | if (mListeners?.contains(listener) == true) { |
| 394 | mListeners?.remove(listener) | 396 | mListeners?.remove(listener) |
| 395 | } | 397 | } |
android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/bean/BaseResultModel.kt
| @@ -19,7 +19,9 @@ class BaseResultModel( | @@ -19,7 +19,9 @@ class BaseResultModel( | ||
| 19 | constructor(parcel: Parcel) : this( | 19 | constructor(parcel: Parcel) : this( |
| 20 | parcel.readString(), | 20 | parcel.readString(), |
| 21 | parcel.readDouble(), | 21 | parcel.readDouble(), |
| 22 | - TODO("scores"), | 22 | + mutableListOf<SingleResultModel>().apply { |
| 23 | + parcel.readTypedList(this, SingleResultModel.CREATOR) | ||
| 24 | + }, | ||
| 23 | parcel.readDouble(), | 25 | parcel.readDouble(), |
| 24 | parcel.readDouble() | 26 | parcel.readDouble() |
| 25 | ) { | 27 | ) { |
| @@ -59,6 +61,7 @@ class BaseResultModel( | @@ -59,6 +61,7 @@ class BaseResultModel( | ||
| 59 | override fun writeToParcel(parcel: Parcel, flags: Int) { | 61 | override fun writeToParcel(parcel: Parcel, flags: Int) { |
| 60 | parcel.writeString(originText) | 62 | parcel.writeString(originText) |
| 61 | parcel.writeDouble(score) | 63 | parcel.writeDouble(score) |
| 64 | + parcel.writeTypedList(scores) | ||
| 62 | parcel.writeDouble(pronounce) | 65 | parcel.writeDouble(pronounce) |
| 63 | parcel.writeDouble(fluency) | 66 | parcel.writeDouble(fluency) |
| 64 | } | 67 | } |
android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/util/GlobalHandler.kt
0 → 100644
| 1 | +package com.kouyuxingqiu.wow_english.util | ||
| 2 | + | ||
| 3 | +import android.os.Handler | ||
| 4 | +import android.os.Looper | ||
| 5 | + | ||
| 6 | +/** | ||
| 7 | + * @author: stay | ||
| 8 | + * @date: 2023/7/1 16:55 | ||
| 9 | + * @description: 全局Handler, 用于切线程等操作 | ||
| 10 | + */ | ||
| 11 | +object GlobalHandler { | ||
| 12 | + private var handler: Handler? = null | ||
| 13 | + | ||
| 14 | + init { | ||
| 15 | + handler = Handler(Looper.getMainLooper()) | ||
| 16 | + } | ||
| 17 | + | ||
| 18 | + fun runOnMainThread(runnable: Runnable?) { | ||
| 19 | + runnable?.let { | ||
| 20 | + handler?.post(it) | ||
| 21 | + } | ||
| 22 | + } | ||
| 23 | +} | ||
| 0 | \ No newline at end of file | 24 | \ No newline at end of file |
ios/Runner/XSMessageMehtodChannel.swift
| @@ -13,7 +13,7 @@ class XSMessageMehtodChannel: NSObject,SSOralEvaluatingManagerDelegate { | @@ -13,7 +13,7 @@ class XSMessageMehtodChannel: NSObject,SSOralEvaluatingManagerDelegate { | ||
| 13 | init(message:FlutterBinaryMessenger) { | 13 | init(message:FlutterBinaryMessenger) { |
| 14 | super.init() | 14 | super.init() |
| 15 | resultData = Dictionary() | 15 | resultData = Dictionary() |
| 16 | - messageChannel = FlutterMethodChannel.init(name: "wow_english/sing_sound_method_channely", binaryMessenger: message) | 16 | + messageChannel = FlutterMethodChannel.init(name: "wow_english/sing_sound_method_channel", binaryMessenger: message) |
| 17 | messageChannel!.setMethodCallHandler { call, result in | 17 | messageChannel!.setMethodCallHandler { call, result in |
| 18 | self.handle(call, result) | 18 | self.handle(call, result) |
| 19 | } | 19 | } |
lib/models/course_process_entity.dart
| @@ -34,6 +34,8 @@ class CourseProcessReadings { | @@ -34,6 +34,8 @@ class CourseProcessReadings { | ||
| 34 | String? picUrl; | 34 | String? picUrl; |
| 35 | int? sortOrder; | 35 | int? sortOrder; |
| 36 | String? word; | 36 | String? word; |
| 37 | + String? recordUrl; | ||
| 38 | + String? recordScore; | ||
| 37 | 39 | ||
| 38 | CourseProcessReadings(); | 40 | CourseProcessReadings(); |
| 39 | 41 |
lib/pages/home/home_page.dart
| @@ -116,6 +116,7 @@ class _HomePageView extends StatelessWidget { | @@ -116,6 +116,7 @@ class _HomePageView extends StatelessWidget { | ||
| 116 | return; | 116 | return; |
| 117 | } | 117 | } |
| 118 | if (data.courseType == 4) {//绘本 | 118 | if (data.courseType == 4) {//绘本 |
| 119 | + Navigator.of(context).pushNamed(AppRouteName.reading, arguments: {'courseLessonId':data.id!}); | ||
| 119 | return; | 120 | return; |
| 120 | } | 121 | } |
| 121 | 122 |
lib/pages/practice/bloc/topic_picture_bloc.dart
| @@ -90,7 +90,7 @@ class TopicPictureBloc extends Bloc<TopicPictureEvent, TopicPictureState> { | @@ -90,7 +90,7 @@ class TopicPictureBloc extends Bloc<TopicPictureEvent, TopicPictureState> { | ||
| 90 | add(VoicePlayStateChangeEvent()); | 90 | add(VoicePlayStateChangeEvent()); |
| 91 | }); | 91 | }); |
| 92 | 92 | ||
| 93 | - methodChannel = const MethodChannel('wow_english/sing_sound_method_channely'); | 93 | + methodChannel = const MethodChannel('wow_english/sing_sound_method_channel'); |
| 94 | methodChannel.setMethodCallHandler((call) async { | 94 | methodChannel.setMethodCallHandler((call) async { |
| 95 | if (call.method == 'voiceResult') {//评测结果 | 95 | if (call.method == 'voiceResult') {//评测结果 |
| 96 | add(XSVoiceResultEvent(call.arguments)); | 96 | add(XSVoiceResultEvent(call.arguments)); |
| @@ -181,7 +181,7 @@ class TopicPictureBloc extends Bloc<TopicPictureEvent, TopicPictureState> { | @@ -181,7 +181,7 @@ class TopicPictureBloc extends Bloc<TopicPictureEvent, TopicPictureState> { | ||
| 181 | 181 | ||
| 182 | ///先声测试 | 182 | ///先声测试 |
| 183 | void _voiceXsTest(XSVoiceTestEvent event,Emitter<TopicPictureState> emitter) async { | 183 | void _voiceXsTest(XSVoiceTestEvent event,Emitter<TopicPictureState> emitter) async { |
| 184 | - audioPlayer.stop(); | 184 | + await audioPlayer.stop(); |
| 185 | methodChannel.invokeMethod( | 185 | methodChannel.invokeMethod( |
| 186 | 'startVoice', | 186 | 'startVoice', |
| 187 | {'word':event.testWord,'type':event.type,'userId':event.userId.toString()} | 187 | {'word':event.testWord,'type':event.type,'userId':event.userId.toString()} |
lib/pages/reading/bloc/reading_bloc.dart
| @@ -10,13 +10,30 @@ import '../../../common/request/dao/listen_dao.dart'; | @@ -10,13 +10,30 @@ import '../../../common/request/dao/listen_dao.dart'; | ||
| 10 | import '../../../common/request/exception.dart'; | 10 | import '../../../common/request/exception.dart'; |
| 11 | import '../../../models/course_process_entity.dart'; | 11 | import '../../../models/course_process_entity.dart'; |
| 12 | import '../../../utils/loading.dart'; | 12 | import '../../../utils/loading.dart'; |
| 13 | +import 'dart:convert'; | ||
| 13 | 14 | ||
| 14 | part 'reading_event.dart'; | 15 | part 'reading_event.dart'; |
| 15 | part 'reading_state.dart'; | 16 | part 'reading_state.dart'; |
| 16 | 17 | ||
| 18 | +enum VoicePlayState { | ||
| 19 | + ///未知 | ||
| 20 | + unKnow, | ||
| 21 | + | ||
| 22 | + ///播放中 | ||
| 23 | + playing, | ||
| 24 | + | ||
| 25 | + ///播放完成 | ||
| 26 | + completed, | ||
| 27 | + | ||
| 28 | + ///播放终止 | ||
| 29 | + stop | ||
| 30 | +} | ||
| 31 | + | ||
| 17 | class ReadingPageBloc extends Bloc<ReadingPageEvent, ReadingPageState> { | 32 | class ReadingPageBloc extends Bloc<ReadingPageEvent, ReadingPageState> { |
| 18 | final PageController pageController; | 33 | final PageController pageController; |
| 19 | 34 | ||
| 35 | + final String courseLessonId; | ||
| 36 | + | ||
| 20 | ///当前页索引 | 37 | ///当前页索引 |
| 21 | int _currentPage = 0; | 38 | int _currentPage = 0; |
| 22 | 39 | ||
| @@ -36,40 +53,98 @@ class ReadingPageBloc extends Bloc<ReadingPageEvent, ReadingPageState> { | @@ -36,40 +53,98 @@ class ReadingPageBloc extends Bloc<ReadingPageEvent, ReadingPageState> { | ||
| 36 | 53 | ||
| 37 | bool get isRecording => _isRecording; | 54 | bool get isRecording => _isRecording; |
| 38 | 55 | ||
| 56 | + ///原始音频是否正在播放 | ||
| 57 | + bool _isOriginAudioPlaying = false; | ||
| 58 | + | ||
| 59 | + bool get isOriginAudioPlaying => _isOriginAudioPlaying; | ||
| 60 | + | ||
| 61 | + ///录音音频是否正在播放 | ||
| 62 | + bool _isRecordAudioPlaying = false; | ||
| 63 | + | ||
| 64 | + bool get isRecordAudioPlaying => _isRecordAudioPlaying; | ||
| 65 | + | ||
| 66 | + ///正在播放音频状态 | ||
| 67 | + VoicePlayState _voicePlayState = VoicePlayState.unKnow; | ||
| 68 | + | ||
| 69 | + VoicePlayState get voicePlayState => _voicePlayState; | ||
| 70 | + | ||
| 39 | late MethodChannel methodChannel; | 71 | late MethodChannel methodChannel; |
| 40 | 72 | ||
| 41 | late AudioPlayer audioPlayer; | 73 | late AudioPlayer audioPlayer; |
| 42 | 74 | ||
| 43 | - ReadingPageBloc(this.pageController) : super(ReadingPageInitial()) { | 75 | + ReadingPageBloc(this.pageController, this.courseLessonId) |
| 76 | + : super(ReadingPageInitial()) { | ||
| 44 | on<CurrentPageIndexChangeEvent>(_pageControllerChange); | 77 | on<CurrentPageIndexChangeEvent>(_pageControllerChange); |
| 45 | - on<CurrentModeChangeEvent>(_selectItemLoad); | 78 | + on<CurrentModeChangeEvent>(_playModeChange); |
| 46 | // pageController.addListener(() { | 79 | // pageController.addListener(() { |
| 47 | // _currentPage = pageController.page!.round(); | 80 | // _currentPage = pageController.page!.round(); |
| 48 | // }); | 81 | // }); |
| 49 | on<RequestDataEvent>(_requestData); | 82 | on<RequestDataEvent>(_requestData); |
| 50 | - on<ReadingPageEvent>((event, emit) { | 83 | + on<InitBlocEvent>((event, emit) { |
| 51 | //音频播放器 | 84 | //音频播放器 |
| 52 | audioPlayer = AudioPlayer(); | 85 | audioPlayer = AudioPlayer(); |
| 53 | - audioPlayer.onPlayerStateChanged.listen((event) { | 86 | + audioPlayer.onPlayerStateChanged.listen((event) async { |
| 87 | + debugPrint('播放状态变化'); | ||
| 54 | if (event == PlayerState.completed) { | 88 | if (event == PlayerState.completed) { |
| 55 | - if (kDebugMode) { | ||
| 56 | - print('绘本播放完成'); | 89 | + debugPrint('播放完成'); |
| 90 | + _voicePlayState = VoicePlayState.completed; | ||
| 91 | + } | ||
| 92 | + if (event == PlayerState.stopped) { | ||
| 93 | + debugPrint('播放结束'); | ||
| 94 | + _voicePlayState = VoicePlayState.stop; | ||
| 95 | + } | ||
| 57 | 96 | ||
| 58 | - } | 97 | + if (event == PlayerState.playing) { |
| 98 | + debugPrint('正在播放中'); | ||
| 99 | + _voicePlayState = VoicePlayState.playing; | ||
| 100 | + } | ||
| 101 | + if (isClosed) { | ||
| 102 | + return; | ||
| 59 | } | 103 | } |
| 104 | + add(VoicePlayStateChangeEvent()); | ||
| 60 | }); | 105 | }); |
| 61 | 106 | ||
| 62 | - methodChannel = const MethodChannel('sing_sound_method_channel'); | ||
| 63 | - methodChannel.invokeMethod('initVoiceSdk',{}); | 107 | + methodChannel = |
| 108 | + const MethodChannel('wow_english/sing_sound_method_channel'); | ||
| 109 | + methodChannel.invokeMethod('initVoiceSdk', {}); //初始化评测 | ||
| 64 | methodChannel.setMethodCallHandler((call) async { | 110 | methodChannel.setMethodCallHandler((call) async { |
| 65 | - if (call.method == 'voiceResult') {//评测结束 | ||
| 66 | - // add(XSVoiceResultEvent(call.arguments)); | 111 | + if (call.method == 'voiceResult') { |
| 112 | + //评测结果 | ||
| 113 | + add(XSVoiceResultEvent(call.arguments)); | ||
| 114 | + return; | ||
| 115 | + } | ||
| 116 | + | ||
| 117 | + if (call.method == 'voiceStart') { | ||
| 118 | + //评测开始 | ||
| 119 | + if (kDebugMode) { | ||
| 120 | + print('评测开始'); | ||
| 121 | + } | ||
| 122 | + return; | ||
| 123 | + } | ||
| 124 | + | ||
| 125 | + if (call.method == 'voiceEnd') { | ||
| 126 | + //评测结束 | ||
| 127 | + if (kDebugMode) { | ||
| 128 | + print('评测结束'); | ||
| 129 | + } | ||
| 130 | + return; | ||
| 131 | + } | ||
| 132 | + | ||
| 133 | + if (call.method == 'voiceFail') { | ||
| 134 | + //评测失败 | ||
| 135 | + EasyLoading.showToast('评测失败'); | ||
| 136 | + return; | ||
| 67 | } | 137 | } |
| 68 | }); | 138 | }); |
| 69 | }); | 139 | }); |
| 140 | + on<VoicePlayStateChangeEvent>(_voicePlayStateChange); | ||
| 70 | on<PlayOriginalAudioEvent>(_playOriginalAudio); | 141 | on<PlayOriginalAudioEvent>(_playOriginalAudio); |
| 71 | - on<XSVoiceTestEvent>(_voiceXsTest); | 142 | + on<XSVoiceInitEvent>(_initVoiceSdk); |
| 143 | + on<XSVoiceStartEvent>(_voiceXsStart); | ||
| 144 | + on<XSVoiceStopEvent>(_voiceXsStop); | ||
| 72 | on<XSVoiceResultEvent>(_voiceXsResult); | 145 | on<XSVoiceResultEvent>(_voiceXsResult); |
| 146 | + | ||
| 147 | + on<PlayRecordAudioEvent>(_playRecordAudio); | ||
| 73 | } | 148 | } |
| 74 | 149 | ||
| 75 | @override | 150 | @override |
| @@ -83,11 +158,11 @@ class ReadingPageBloc extends Bloc<ReadingPageEvent, ReadingPageState> { | @@ -83,11 +158,11 @@ class ReadingPageBloc extends Bloc<ReadingPageEvent, ReadingPageState> { | ||
| 83 | void _pageControllerChange(CurrentPageIndexChangeEvent event, | 158 | void _pageControllerChange(CurrentPageIndexChangeEvent event, |
| 84 | Emitter<ReadingPageState> emitter) async { | 159 | Emitter<ReadingPageState> emitter) async { |
| 85 | _currentPage = event.pageIndex; | 160 | _currentPage = event.pageIndex; |
| 86 | - _playOriginVoice(null); | 161 | + _playOriginalAudioInner(null); |
| 87 | emitter(CurrentPageIndexState()); | 162 | emitter(CurrentPageIndexState()); |
| 88 | } | 163 | } |
| 89 | 164 | ||
| 90 | - void _selectItemLoad( | 165 | + void _playModeChange( |
| 91 | CurrentModeChangeEvent event, Emitter<ReadingPageState> emitter) async { | 166 | CurrentModeChangeEvent event, Emitter<ReadingPageState> emitter) async { |
| 92 | if (_currentMode == ReadingModeType.auto) { | 167 | if (_currentMode == ReadingModeType.auto) { |
| 93 | _currentMode = ReadingModeType.manual; | 168 | _currentMode = ReadingModeType.manual; |
| @@ -102,7 +177,7 @@ class ReadingPageBloc extends Bloc<ReadingPageEvent, ReadingPageState> { | @@ -102,7 +177,7 @@ class ReadingPageBloc extends Bloc<ReadingPageEvent, ReadingPageState> { | ||
| 102 | RequestDataEvent event, Emitter<ReadingPageState> emitter) async { | 177 | RequestDataEvent event, Emitter<ReadingPageState> emitter) async { |
| 103 | try { | 178 | try { |
| 104 | await loading(() async { | 179 | await loading(() async { |
| 105 | - _entity = await ListenDao.process('1'); | 180 | + _entity = await ListenDao.process(courseLessonId); |
| 106 | print("reading page entity: ${_entity!.toJson()}"); | 181 | print("reading page entity: ${_entity!.toJson()}"); |
| 107 | emitter(RequestDataState()); | 182 | emitter(RequestDataState()); |
| 108 | }); | 183 | }); |
| @@ -113,20 +188,36 @@ class ReadingPageBloc extends Bloc<ReadingPageEvent, ReadingPageState> { | @@ -113,20 +188,36 @@ class ReadingPageBloc extends Bloc<ReadingPageEvent, ReadingPageState> { | ||
| 113 | } | 188 | } |
| 114 | } | 189 | } |
| 115 | 190 | ||
| 116 | - void _playOriginalAudio(PlayOriginalAudioEvent event, Emitter<ReadingPageState> emitter) async { | ||
| 117 | - print("_playOriginalAudio"); | ||
| 118 | - _playOriginVoice(event.url); | 191 | + /// 播放绘本原音 |
| 192 | + void _playOriginalAudio( | ||
| 193 | + PlayOriginalAudioEvent event, Emitter<ReadingPageState> emitter) async { | ||
| 194 | + _playOriginalAudioInner(event.url); | ||
| 195 | + } | ||
| 196 | + | ||
| 197 | + void _playOriginalAudioInner(String? audioUrl) { | ||
| 198 | + print("_playOriginalAudio url=$audioUrl"); | ||
| 199 | + audioUrl ??= currentPageData()?.audioUrl ?? ''; | ||
| 200 | + _playAudio(audioUrl); | ||
| 201 | + _isOriginAudioPlaying = true; | ||
| 119 | } | 202 | } |
| 120 | 203 | ||
| 121 | - /// 播放绘本原音 | ||
| 122 | - void _playOriginVoice(String? audioUrl) async { | ||
| 123 | - audioPlayer.stop(); | ||
| 124 | - final readingData = currentPageData(); | ||
| 125 | - if (readingData?.audioUrl != null) { | ||
| 126 | - final urlStr = audioUrl ?? readingData?.audioUrl ?? ''; | ||
| 127 | - if (urlStr.isNotEmpty) { | ||
| 128 | - audioPlayer.play(UrlSource(urlStr)); | ||
| 129 | - } | 204 | + /// 播放录音 |
| 205 | + void _playRecordAudio( | ||
| 206 | + PlayRecordAudioEvent event, Emitter<ReadingPageState> emitter) async { | ||
| 207 | + _playRecordAudioInner(); | ||
| 208 | + } | ||
| 209 | + | ||
| 210 | + void _playRecordAudioInner() { | ||
| 211 | + final recordAudioUrl = currentPageData()?.recordUrl; | ||
| 212 | + print("_playRecordAudioInner url=${currentPageData()?.recordUrl}"); | ||
| 213 | + _playAudio(recordAudioUrl); | ||
| 214 | + _isRecordAudioPlaying = true; | ||
| 215 | + } | ||
| 216 | + | ||
| 217 | + void _playAudio(String? audioUrl) async { | ||
| 218 | + await audioPlayer.stop(); | ||
| 219 | + if (audioUrl!.isNotEmpty) { | ||
| 220 | + await audioPlayer.play(UrlSource(audioUrl)); | ||
| 130 | } | 221 | } |
| 131 | } | 222 | } |
| 132 | 223 | ||
| @@ -139,8 +230,16 @@ class ReadingPageBloc extends Bloc<ReadingPageEvent, ReadingPageState> { | @@ -139,8 +230,16 @@ class ReadingPageBloc extends Bloc<ReadingPageEvent, ReadingPageState> { | ||
| 139 | return _entity?.readings?[_currentPage]; | 230 | return _entity?.readings?[_currentPage]; |
| 140 | } | 231 | } |
| 141 | 232 | ||
| 233 | + ///初始化SDK | ||
| 234 | + _initVoiceSdk( | ||
| 235 | + XSVoiceInitEvent event, Emitter<ReadingPageState> emitter) async { | ||
| 236 | + methodChannel.invokeMethod('initVoiceSdk', event.data); | ||
| 237 | + } | ||
| 238 | + | ||
| 142 | ///先声测试 | 239 | ///先声测试 |
| 143 | - void _voiceXsTest(XSVoiceTestEvent event, Emitter<ReadingPageState> emitter) async { | 240 | + void _voiceXsStart( |
| 241 | + XSVoiceStartEvent event, Emitter<ReadingPageState> emitter) async { | ||
| 242 | + _stopAudio(); | ||
| 144 | startRecord(event.content); | 243 | startRecord(event.content); |
| 145 | emitter(XSVoiceTestState()); | 244 | emitter(XSVoiceTestState()); |
| 146 | } | 245 | } |
| @@ -149,24 +248,44 @@ class ReadingPageBloc extends Bloc<ReadingPageEvent, ReadingPageState> { | @@ -149,24 +248,44 @@ class ReadingPageBloc extends Bloc<ReadingPageEvent, ReadingPageState> { | ||
| 149 | if (_isRecording == true) { | 248 | if (_isRecording == true) { |
| 150 | return; | 249 | return; |
| 151 | } | 250 | } |
| 152 | - EasyLoading.show(status: '录音中....'); | ||
| 153 | methodChannel.invokeMethod( | 251 | methodChannel.invokeMethod( |
| 154 | - 'startVoice', | ||
| 155 | - {'word':'how old are you','type':'0','userId':'1'} | ||
| 156 | - ); | 252 | + 'startVoice', {'word': 'how old are you', 'type': '0', 'userId': '1'}); |
| 157 | _isRecording = true; | 253 | _isRecording = true; |
| 158 | } | 254 | } |
| 159 | 255 | ||
| 160 | - void _voiceXsResult(XSVoiceResultEvent event,Emitter<ReadingPageState> emitter) async { | ||
| 161 | - final Map args = event.message as Map; | ||
| 162 | - final result = args['result'] as String; | ||
| 163 | - if (result == '1') { | ||
| 164 | - final overall = args['overall'].toString(); | ||
| 165 | - EasyLoading.showToast('测评成功,分数是$overall',duration: const Duration(seconds: 10)); | 256 | + void _voiceXsResult( |
| 257 | + XSVoiceResultEvent event, Emitter<ReadingPageState> emitter) async { | ||
| 258 | + final Map args = json.decode(event.message); | ||
| 259 | + final result = args['result']; | ||
| 260 | + print("_voiceXsResult result=${result}"); | ||
| 261 | + if (result != null) { | ||
| 262 | + final overall = result['overall'].toString(); | ||
| 263 | + EasyLoading.showToast('测评成功,分数是$overall', | ||
| 264 | + duration: const Duration(seconds: 10)); | ||
| 265 | + currentPageData()?.recordScore = overall; | ||
| 266 | + currentPageData()?.recordUrl = args['audioUrl'] + '.mp3'; | ||
| 267 | + _playRecordAudioInner(); | ||
| 166 | } else { | 268 | } else { |
| 167 | - EasyLoading.showToast('测评失败',duration: const Duration(seconds: 10)); | 269 | + EasyLoading.showToast('测评失败', duration: const Duration(seconds: 10)); |
| 168 | } | 270 | } |
| 169 | _isRecording = false; | 271 | _isRecording = false; |
| 170 | emitter(XSVoiceTestState()); | 272 | emitter(XSVoiceTestState()); |
| 171 | } | 273 | } |
| 274 | + | ||
| 275 | + ///终止评测 | ||
| 276 | + void _voiceXsStop( | ||
| 277 | + XSVoiceStopEvent event, Emitter<ReadingPageState> emitter) async { | ||
| 278 | + methodChannel.invokeMethod('stopVoice'); | ||
| 279 | + } | ||
| 280 | + | ||
| 281 | + void _voicePlayStateChange(VoicePlayStateChangeEvent event, | ||
| 282 | + Emitter<ReadingPageState> emitter) async { | ||
| 283 | + emitter(VoicePlayStateChange()); | ||
| 284 | + } | ||
| 285 | + | ||
| 286 | + void _stopAudio() async { | ||
| 287 | + await audioPlayer.stop(); | ||
| 288 | + _isOriginAudioPlaying = false; | ||
| 289 | + _isRecordAudioPlaying = false; | ||
| 290 | + } | ||
| 172 | } | 291 | } |
lib/pages/reading/bloc/reading_event.dart
| @@ -3,6 +3,9 @@ part of 'reading_bloc.dart'; | @@ -3,6 +3,9 @@ part of 'reading_bloc.dart'; | ||
| 3 | @immutable | 3 | @immutable |
| 4 | abstract class ReadingPageEvent {} | 4 | abstract class ReadingPageEvent {} |
| 5 | 5 | ||
| 6 | +///页面初始化 | ||
| 7 | +class InitBlocEvent extends ReadingPageEvent {} | ||
| 8 | + | ||
| 6 | class CurrentPageIndexChangeEvent extends ReadingPageEvent { | 9 | class CurrentPageIndexChangeEvent extends ReadingPageEvent { |
| 7 | final int pageIndex; | 10 | final int pageIndex; |
| 8 | CurrentPageIndexChangeEvent(this.pageIndex); | 11 | CurrentPageIndexChangeEvent(this.pageIndex); |
| @@ -32,9 +35,18 @@ class XSVoiceResultEvent extends ReadingPageEvent { | @@ -32,9 +35,18 @@ class XSVoiceResultEvent extends ReadingPageEvent { | ||
| 32 | } | 35 | } |
| 33 | 36 | ||
| 34 | ///先声测试 | 37 | ///先声测试 |
| 35 | -class XSVoiceTestEvent extends ReadingPageEvent { | 38 | +class XSVoiceStartEvent extends ReadingPageEvent { |
| 36 | final String content; | 39 | final String content; |
| 37 | final String type; | 40 | final String type; |
| 38 | final String userId; | 41 | final String userId; |
| 39 | - XSVoiceTestEvent(this.content,this.type,this.userId); | ||
| 40 | -} | ||
| 41 | \ No newline at end of file | 42 | \ No newline at end of file |
| 43 | + XSVoiceStartEvent(this.content,this.type,this.userId); | ||
| 44 | +} | ||
| 45 | + | ||
| 46 | +///先声评测停止 | ||
| 47 | +class XSVoiceStopEvent extends ReadingPageEvent {} | ||
| 48 | + | ||
| 49 | +///音频播放状态 | ||
| 50 | +class VoicePlayStateChangeEvent extends ReadingPageEvent {} | ||
| 51 | + | ||
| 52 | +///录音播放 | ||
| 53 | +class PlayRecordAudioEvent extends ReadingPageEvent {} | ||
| 42 | \ No newline at end of file | 54 | \ No newline at end of file |
lib/pages/reading/bloc/reading_state.dart
| @@ -13,3 +13,5 @@ class CurrentModeState extends ReadingPageState {} | @@ -13,3 +13,5 @@ class CurrentModeState extends ReadingPageState {} | ||
| 13 | class RequestDataState extends ReadingPageState {} | 13 | class RequestDataState extends ReadingPageState {} |
| 14 | 14 | ||
| 15 | class XSVoiceTestState extends ReadingPageState {} | 15 | class XSVoiceTestState extends ReadingPageState {} |
| 16 | + | ||
| 17 | +class VoicePlayStateChange extends ReadingPageState {} |
lib/pages/reading/reading_page.dart
| @@ -9,12 +9,16 @@ import '../../models/course_process_entity.dart'; | @@ -9,12 +9,16 @@ import '../../models/course_process_entity.dart'; | ||
| 9 | import 'bloc/reading_bloc.dart'; | 9 | import 'bloc/reading_bloc.dart'; |
| 10 | 10 | ||
| 11 | class ReadingPage extends StatelessWidget { | 11 | class ReadingPage extends StatelessWidget { |
| 12 | - const ReadingPage({super.key}); | 12 | + const ReadingPage({super.key, this.courseLessonId}); |
| 13 | + | ||
| 14 | + final String? courseLessonId; | ||
| 13 | 15 | ||
| 14 | @override | 16 | @override |
| 15 | Widget build(BuildContext context) { | 17 | Widget build(BuildContext context) { |
| 16 | return BlocProvider( | 18 | return BlocProvider( |
| 17 | - create: (_) => ReadingPageBloc(PageController())..add(RequestDataEvent()), | 19 | + create: (_) => ReadingPageBloc(PageController(), courseLessonId ?? '') |
| 20 | + ..add(InitBlocEvent()) | ||
| 21 | + ..add(RequestDataEvent()), | ||
| 18 | child: _ReadingPage(), | 22 | child: _ReadingPage(), |
| 19 | ); | 23 | ); |
| 20 | } | 24 | } |
| @@ -135,11 +139,13 @@ class _ReadingPage extends StatelessWidget { | @@ -135,11 +139,13 @@ class _ReadingPage extends StatelessWidget { | ||
| 135 | if (bloc.isRecording) { | 139 | if (bloc.isRecording) { |
| 136 | return; | 140 | return; |
| 137 | } | 141 | } |
| 138 | - print("voice tap"); | ||
| 139 | bloc.add(PlayOriginalAudioEvent(null)); | 142 | bloc.add(PlayOriginalAudioEvent(null)); |
| 140 | }, | 143 | }, |
| 141 | child: Image.asset( | 144 | child: Image.asset( |
| 142 | - 'voice'.assetPng, | 145 | + bloc.voicePlayState == VoicePlayState.playing && |
| 146 | + bloc.isOriginAudioPlaying | ||
| 147 | + ? 'reade_answer'.assetGif | ||
| 148 | + : 'voice'.assetPng, | ||
| 143 | height: 40.h, | 149 | height: 40.h, |
| 144 | width: 45.w, | 150 | width: 45.w, |
| 145 | ), | 151 | ), |
| @@ -161,9 +167,13 @@ class _ReadingPage extends StatelessWidget { | @@ -161,9 +167,13 @@ class _ReadingPage extends StatelessWidget { | ||
| 161 | GestureDetector( | 167 | GestureDetector( |
| 162 | onTap: () { | 168 | onTap: () { |
| 163 | if (bloc.isRecording) { | 169 | if (bloc.isRecording) { |
| 164 | - return; | 170 | + bloc.add(XSVoiceStopEvent()); |
| 171 | + } else { | ||
| 172 | + bloc.add(XSVoiceStartEvent( | ||
| 173 | + bloc.currentPageData()?.word ?? '', | ||
| 174 | + '0', | ||
| 175 | + UserUtil.getUser()!.id.toString())); | ||
| 165 | } | 176 | } |
| 166 | - bloc.add(XSVoiceTestEvent(bloc.currentPageData()?.word??'', '0',UserUtil.getUser()!.id.toString())); | ||
| 167 | }, | 177 | }, |
| 168 | child: Image.asset( | 178 | child: Image.asset( |
| 169 | 'micro_phone'.assetPng, | 179 | 'micro_phone'.assetPng, |
| @@ -173,18 +183,23 @@ class _ReadingPage extends StatelessWidget { | @@ -173,18 +183,23 @@ class _ReadingPage extends StatelessWidget { | ||
| 173 | SizedBox( | 183 | SizedBox( |
| 174 | width: 10.w, | 184 | width: 10.w, |
| 175 | ), | 185 | ), |
| 176 | - Visibility( | ||
| 177 | - visible: false, | ||
| 178 | - | ||
| 179 | - ///todo 依据是否录过音 | ||
| 180 | - child: Image.asset( | ||
| 181 | - 'record_pause'.assetWebp, | ||
| 182 | - | ||
| 183 | - ///todo 根据播放状态切换图片 | ||
| 184 | - height: 33.h, | ||
| 185 | - width: 33.w, | ||
| 186 | - ), | ||
| 187 | - ) | 186 | + GestureDetector( |
| 187 | + onTap: () { | ||
| 188 | + if (bloc.isRecording) { | ||
| 189 | + return; | ||
| 190 | + } | ||
| 191 | + bloc.add(PlayRecordAudioEvent()); | ||
| 192 | + }, | ||
| 193 | + child: Visibility( | ||
| 194 | + visible: bloc.currentPageData()?.recordUrl != null, | ||
| 195 | + child: Image.asset( | ||
| 196 | + bloc.isRecordAudioPlaying | ||
| 197 | + ? 'record_pause'.assetWebp | ||
| 198 | + : 'record_play'.assetWebp, | ||
| 199 | + height: 33.h, | ||
| 200 | + width: 33.w, | ||
| 201 | + ), | ||
| 202 | + )), | ||
| 188 | ], | 203 | ], |
| 189 | ), | 204 | ), |
| 190 | ), | 205 | ), |
lib/route/route.dart
| @@ -139,7 +139,11 @@ class AppRouter { | @@ -139,7 +139,11 @@ class AppRouter { | ||
| 139 | pageBuilder: (_, __, ___) => const TabPage(), | 139 | pageBuilder: (_, __, ___) => const TabPage(), |
| 140 | transitionsBuilder: (_, __, ___, child) => child); | 140 | transitionsBuilder: (_, __, ___, child) => child); |
| 141 | case AppRouteName.reading: | 141 | case AppRouteName.reading: |
| 142 | - return CupertinoPageRoute(builder: (_) => const ReadingPage()); | 142 | + var courseLessonId = ''; |
| 143 | + if (settings.arguments != null) { | ||
| 144 | + courseLessonId = (settings.arguments as Map)['courseLessonId'] as String??''; | ||
| 145 | + } | ||
| 146 | + return CupertinoPageRoute(builder: (_) => ReadingPage(courseLessonId: courseLessonId)); | ||
| 143 | default: | 147 | default: |
| 144 | return CupertinoPageRoute( | 148 | return CupertinoPageRoute( |
| 145 | builder: (_) => Scaffold(body: Center(child: Text('No route defined for ${settings.name}')))); | 149 | builder: (_) => Scaffold(body: Center(child: Text('No route defined for ${settings.name}')))); |