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}')))); |