Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

为什么要在onSynthesizeText方法整个代码块上加同步锁? #20

Closed
GeminiT369 opened this issue Dec 6, 2022 · 51 comments
Closed

Comments

@GeminiT369
Copy link

如题,为什么要在onSynthesizeText方法整个代码块上加同步锁?这样不是将本来可以并发合成的方法,变成了一个同步的方法,一次只处理一个请求?阅读实际上一次发送了多个合成请求,如果并行处理的话,在朗读第二段以及之后的段之前,实际音频已经合成了,这样朗读段之间就不会有网络请求造成的延迟。

@jing332
Copy link
Owner

jing332 commented Dec 6, 2022 via email

@jing332
Copy link
Owner

jing332 commented Dec 6, 2022

其实加不加锁都一样 在写入音频那里也会阻塞。
我也忘记当初为何要加锁了

@jing332
Copy link
Owner

jing332 commented Dec 6, 2022

刚测试了下 确实可以提前获取到后面文本
不过我还得想想怎么实现缓存
总之感谢你的提醒

@limne
Copy link

limne commented Dec 6, 2022

客户端应用不用考虑并发,搞得效率高了,微软限制就来了

@jing332
Copy link
Owner

jing332 commented Dec 6, 2022 via email

@limne
Copy link

limne commented Dec 6, 2022

微软又收紧限制了,下午听一个小时就不行了

@jing332
Copy link
Owner

jing332 commented Dec 6, 2022 via email

@jing332
Copy link
Owner

jing332 commented Dec 6, 2022

我研究了一会 发现缓存实现没那么容易
最主要的的是 start()audioAvailable() 都必须在合成线程上调用,而 audioAvailable() 写入音频这个方法还是阻塞的

官方文档: 告知服务从给定文本合成语音。此方法应阻塞直到 合成完成。在合成线程上调用。

image

@jing332
Copy link
Owner

jing332 commented Dec 6, 2022

我也是弄不明白 既然 onSynthesizeText() 必须阻塞直到合成完成,那 TextToSpeech.speak() 的队列参数又有啥意义?

Android: 合成必须是同步的,这意味着引擎不得在方法返回后保留回调或对其调用任何方法

@jing332
Copy link
Owner

jing332 commented Dec 6, 2022

这回是没戏了😟

@GeminiT369
Copy link
Author

这回是没戏了😟

可以啊,可以把每次请求和callback提交给一个新线程,只是要注意只能在新线程中调用callback

@jing332
Copy link
Owner

jing332 commented Dec 6, 2022 via email

@GeminiT369
Copy link
Author

GeminiT369 commented Dec 6, 2022

但是新线程不是合成线程,就算调用start和音频写入方法也没反应

参考一下我的写法

        synthesizer.synthesize(speed, volume, text, 1, new SynthesizerCallback() {
            @Override
            public void onBusy() {
                callback.error(TextToSpeech.ERROR_SYNTHESIS);
                Log.d(TAG, "onBusy: engine is busy.");
            }

            @Override
            public void onStart() {
                callback.start(16000, AudioFormat.ENCODING_PCM_16BIT, 1);
                Log.d(TAG, "onStart: everything is ok.");
            }

            @Override
            public void onFinish(String msg) {
                callback.done();
                Log.d(TAG, "onFinish: " + msg);
            }

            @Override
            public void onFailed(String msg) {
                Log.e(TAG, "onFailed: " + msg);
                callback.error(TextToSpeech.ERROR_SERVICE);
            }

            @Override
            public void onSynthesize(byte[] audioData, int index, int size) {
                int offset = 0;
                int length = audioData.length;
                final int maxBufferSize = callback.getMaxBufferSize();
                while (offset < length) {
                    int bytesToWrite = Math.min(maxBufferSize, length - offset);
                    callback.audioAvailable(audioData, offset, bytesToWrite);
                    offset += bytesToWrite;
                }
            }
        });

synthesize方法会启动一个线程,callback 只在新线程中调用,不然播放不了。

@GeminiT369
Copy link
Author

GeminiT369 commented Dec 6, 2022

但是新线程不是合成线程,就算调用start和音频写入方法也没反应

只要callback的start和audioAvailable、done方法调用的是同一个线程就行,不能分开在不同线程调用

@jing332
Copy link
Owner

jing332 commented Dec 6, 2022

但是新线程不是合成线程,就算调用start和音频写入方法也没反应

只要callback的start和audioAvailable、done方法调用的是同一个线程就行

emmm但为什么我已经调用了start() & audioAvailable() 但不播放呢

mScope.launch(Dispatchers.Default) {
            Log.e(TAG, "current thread: ${Thread.currentThread().name}")
            // 接收者
            for (v in audioChannel) {
                val callback = v.first
                callback.start(mAudioFormat.sampleRate, AudioFormat.ENCODING_PCM_16BIT, 1)
                if (v.second == null) {
                    Log.w(TAG, "音频为空")
                    continue
                }
                mAudioDecoder.doDecode(v.second!!, mAudioFormat.sampleRate, {
                    println("writeToCallBack: ${it.size}")
                    writeToCallBack(callback, it)
                }, {})
                callback.done()
            }
        }

@jing332
Copy link
Owner

jing332 commented Dec 6, 2022

launch(Dispatchers.Default) 直接用协程 launch 不行么?

@GeminiT369
Copy link
Author

doDecode

你的doDecode方法是不是启动别的线程了?

@jing332
Copy link
Owner

jing332 commented Dec 6, 2022

没有的,
而且正常来说调用 start() 后阅读APP当前段落就会高亮,然而并无反应

@jing332
Copy link
Owner

jing332 commented Dec 6, 2022

start() & audioAvailable() 返回值也是 = TextToSpeech.SUCCESS

 /* 写入PCM音频到系统组件 */
    private fun writeToCallBack(callback: SynthesisCallback, pcmData: ByteArray) {
        try {
            val maxBufferSize: Int = callback.maxBufferSize
            var offset = 0
            while (offset < pcmData.size) {
                val bytesToWrite = maxBufferSize.coerceAtMost(pcmData.size - offset)
                val ret = callback.audioAvailable(pcmData, offset, bytesToWrite)
                Log.e(TAG, "audioAvailable: $ret")
                offset += bytesToWrite
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

@jing332
Copy link
Owner

jing332 commented Dec 6, 2022

新建了一个线程也不行 mScope.launch(newSingleThreadContext("new-thread-name"))
可能是callback在 onSynthesizeText() return后就不能用了?

@GeminiT369
Copy link
Author

新建了一个线程也不行 mScope.launch(newSingleThreadContext("new-thread-name")) 可能是callback在 onSynthesizeText() return后就不能用了?

要不你试试每次callback调用打印线程编号试试?我也看不出什么问题。。。

@jing332
Copy link
Owner

jing332 commented Dec 6, 2022

public abstract int audioAvailable(byte[] buffer, int offset, int length )
This method should only be called on the synthesis thread, while in TextToSpeechService.onSynthesizeText.

看来必须在 synthesis 线程上调用

@GeminiT369
Copy link
Author

GeminiT369 commented Dec 6, 2022

public abstract int audioAvailable(byte[] buffer, int offset, int length )
This method should only be called on the synthesis thread, while in TextToSpeechService.onSynthesizeText.

看来必须在 synthesis 线程上调用

我之前遇到过无法正常播放的问题,是callback在不同线程调用造成的。有试过直接把原来的synchronized块换成new Thread(lambda).start()吗?

@GeminiT369
Copy link
Author

public abstract int audioAvailable(byte[] buffer, int offset, int length )
This method should only be called on the synthesis thread, while in TextToSpeechService.onSynthesizeText.

看来必须在 synthesis 线程上调用

也有可能是其他非同步方法,并发调用时的问题。。。

@jing332
Copy link
Owner

jing332 commented Dec 6, 2022

测试是否必须在合成线程调用

 override fun onSynthesizeText(request: SynthesisRequest?, callback: SynthesisCallback?) {
        Log.e(TAG, request?.charSequenceText.toString())
        Log.e(TAG, "onSynthesizeText thread: ${Thread.currentThread().id}")
        GlobalScope.launch(newSingleThreadContext("thread")) {
            Log.e(TAG, "callback?.start thread: ${Thread.currentThread().id}")
            callback?.start(16000, AudioFormat.ENCODING_PCM_16BIT, 1)
            val audio = MsTTS().getAudio("测试文本")

            AudioDecoder().doDecode(audio!!, 24000, {
                val maxBufferSize: Int = callback!!.maxBufferSize
                var offset = 0
                while (offset < it.size) {
                    val bytesToWrite = maxBufferSize.coerceAtMost(it.size - offset)
                    val ret = callback.audioAvailable(it, offset, bytesToWrite)
                    Log.e(TAG, "audioAvailable: $ret")
                    offset += bytesToWrite
                }
            }, {})
        }
        Thread.sleep(1000)

播放正常了 输出:

                                                                                        onSynthesizeText thread: 30951
21:18:56.315 SysTtsService                                                E  callback?.start thread: 30961
21:18:56.320 GoLog                                                        E  time="2022-12-06T13:18:56Z" level=info msg="创建WebSocket连接(Edge)..."
21:18:56.320 GoLog                                                        E  time="2022-12-06T13:18:56Z" level=info msg="连接到IP地址: 202.89.233.103:443"
21:18:56.850 AudioDecode                                                  D  找到音频流的index:0, mime:audio/mpeg

21:18:56.896 SysTtsService                                                E  decode thread: 30961
21:18:56.896 SysTtsService                                                E  audioAvailable: 0
21:18:56.897 SysTtsService                                                E  decode thread: 30961
21:18:56.898 SysTtsService                                                E  audioAvailable: 0

看来不一定非要在合成线程调用

@GeminiT369
Copy link
Author

GeminiT369 commented Dec 6, 2022

测试是否必须在合成线程调用

 override fun onSynthesizeText(request: SynthesisRequest?, callback: SynthesisCallback?) {
        Log.e(TAG, request?.charSequenceText.toString())
        Log.e(TAG, "onSynthesizeText thread: ${Thread.currentThread().id}")
        GlobalScope.launch(newSingleThreadContext("thread")) {
            Log.e(TAG, "callback?.start thread: ${Thread.currentThread().id}")
            callback?.start(16000, AudioFormat.ENCODING_PCM_16BIT, 1)
            val audio = MsTTS().getAudio("测试文本")

            AudioDecoder().doDecode(audio!!, 24000, {
                val maxBufferSize: Int = callback!!.maxBufferSize
                var offset = 0
                while (offset < it.size) {
                    val bytesToWrite = maxBufferSize.coerceAtMost(it.size - offset)
                    val ret = callback.audioAvailable(it, offset, bytesToWrite)
                    Log.e(TAG, "audioAvailable: $ret")
                    offset += bytesToWrite
                }
            }, {})
        }
        Thread.sleep(1000)

播放正常了 输出:

                                                                                        onSynthesizeText thread: 30951
21:18:56.315 SysTtsService                                                E  callback?.start thread: 30961
21:18:56.320 GoLog                                                        E  time="2022-12-06T13:18:56Z" level=info msg="创建WebSocket连接(Edge)..."
21:18:56.320 GoLog                                                        E  time="2022-12-06T13:18:56Z" level=info msg="连接到IP地址: 202.89.233.103:443"
21:18:56.850 AudioDecode                                                  D  找到音频流的index:0, mime:audio/mpeg

21:18:56.896 SysTtsService                                                E  decode thread: 30961
21:18:56.896 SysTtsService                                                E  audioAvailable: 0
21:18:56.897 SysTtsService                                                E  decode thread: 30961
21:18:56.898 SysTtsService                                                E  audioAvailable: 0

看来不一定非要在合成线程调用

对,看了之前的问题是非同步方法并发调用的问题,可以给一些非同步方法加锁试试

@jing332
Copy link
Owner

jing332 commented Dec 6, 2022

不像是这个问题 接收者是阻塞的 audioDecode.doDecode() 也会一直等待到播放完毕

我刚才测试了下callback在return后是否有效,测试下来确实是可以正常播放的

@GeminiT369
Copy link
Author

其实单线程也行,每次调用onSynthesizeText时把请求加入处理队列后直接返回;然后用一个新线程,当队列不为空时,从队列中取出请求进行合成

@GeminiT369
Copy link
Author

这样就不会有同步的问题了

@jing332
Copy link
Owner

jing332 commented Dec 6, 2022

我用的是 Kotlin Channel 生产者消费者模式,其实也不会有线程同步问题,整个接收者都在一个单独线程内

@GeminiT369
Copy link
Author

我用的是 Kotlin Channel 生产者消费者模式,其实也不会有线程同步问题,整个接收者都在一个单独线程内

雀食,audioAvailable方法貌似是阻塞的

@jing332
Copy link
Owner

jing332 commented Dec 7, 2022

找到问题了哈哈😂 是首次请求由于没过滤空文本导致返回音频为空,然后continue时忘记 callback.done() 了就导致以后的音频callback一直阻塞。

GlobalScope.launch(newSingleThreadContext("new-thread")) {
            for (v in audioChannel) {
                val callback = v.first
                val audio = v.second
                
                val ret = callback.start(mAudioFormat.sampleRate, AudioFormat.ENCODING_PCM_16BIT, 1)
                if (audio == null) {
                    Log.w(TAG, "音频为空")
                    callback.done()
                    continue
                }
                Log.e(TAG, "音频大小: ${audio.size}")

                mAudioDecoder.doDecode(audio, mAudioFormat.sampleRate, {
                    println("writeToCallBack: ${it.size}")
                    writeToCallBack(callback, it)
                }, {})
                callback.done()
            }
        }

@jing332
Copy link
Owner

jing332 commented Dec 7, 2022

又遇到问题了 =-=
onStop() 在用户暂停时不调用,导致一直播放 只能关闭朗读
经过测试发现 必须要让 onSynthesizeText() 阻塞,否则系统不调用 onStop()

@GeminiT369
Copy link
Author

又遇到问题了 =-= onStop() 在用户暂停时不调用,导致一直播放 只能关闭朗读 经过测试发现 必须要让 onSynthesizeText() 阻塞,否则系统不调用 onStop()

啊,是这样吗?那么正常暂停朗读系统是怎么处理的呢,是使正在调用的audioAvailable无效并返回异常吗?

@jing332
Copy link
Owner

jing332 commented Dec 7, 2022

正常是 onSynthesizeText() 一直阻塞等待播放完毕,如果这时用户暂停,系统就会调用 onStop(),然后就进行一些取消操作。

@jing332
Copy link
Owner

jing332 commented Dec 7, 2022

但如果 onSynthesizeText() return后,拿着它的 callbackrequest 进行合成和写入音频,这时候用户暂停, 当前callback就会失效立刻停止播放,但后面预先缓存的callback还能正常写入播放

@jing332
Copy link
Owner

jing332 commented Dec 7, 2022

我还尝试通过判断返回值确定是否暂停,但就算暂停了也始终=SUCCESS

@GeminiT369
Copy link
Author

但如果 onSynthesizeText() return后,拿着它的 callbackrequest 进行合成和写入音频,这时候用户暂停, 当前callback就会失效立刻停止播放,但后面预先缓存的callback还能正常写入播放

那就在同一个线程播放,在另一个线程合成?

@jing332
Copy link
Owner

jing332 commented Dec 7, 2022

生产者消费者本就是两个线程,问题在于 需要通过 onSynthesizeText() 获取后续所有的段落文本,而我又不能去阻塞它。
安卓文档也说:

Synthesis must be synchronous which means the engine must NOT hold on to the callback or call any methods on it after the method returns.
合成必须是同步的,这意味着引擎不能保留回调,也不能在方法返回后对其调用任何方法。

@jing332
Copy link
Owner

jing332 commented Dec 7, 2022

虽说缓存实现了,但这不能暂停太影响体验了,实在不行只能放弃了 😶

@GeminiT369
Copy link
Author

虽说缓存实现了,但这不能暂停太影响体验了,实在不行只能放弃了 😶

行吧,系统不让这么做放弃也行

@GeminiT369
Copy link
Author

生产者消费者本就是两个线程,问题在于 需要通过 onSynthesizeText() 获取后续所有的段落文本,而我又不能去阻塞它。 安卓文档也说:

Synthesis must be synchronous which means the engine must NOT hold on to the callback or call any methods on it after the method returns.
合成必须是同步的,这意味着引擎不能保留回调,也不能在方法返回后对其调用任何方法。

你没明白我的意思,我想说的是自己写一个生产者消费者模型,一旦某一个callback失效,就终止所有合成和后续播放

@jing332
Copy link
Owner

jing332 commented Dec 7, 2022 via email

@jing332
Copy link
Owner

jing332 commented Dec 7, 2022 via email

@GeminiT369
Copy link
Author

问题是callback我没办法知道它是否失效

如果朗读停止后,依旧通过callback.audioAvailable写入音频数据,音频不会被播放且返回码为-1,正常为0

@GeminiT369
Copy link
Author

问题是callback我没办法知道它是否失效

如果朗读停止后,依旧通过callback.audioAvailable写入音频数据,音频不会被播放且返回码为-1,正常为0

好吧,不阻塞时不行

@jing332
Copy link
Owner

jing332 commented Dec 7, 2022

就算暂停后callback.audioAvailable()也是返回 0 SUCCESS,所以说没法知道是否暂停

@GeminiT369
Copy link
Author

有个不成熟的想法,如果你还想试试的话可以看看。在onSynthesizeText方法内,在方法的最后让最后一个请求线程阻塞,之前的线程返回。实现类似这样:

	private volatile boolean isLast = true;
	private volatile boolean isDone = false;
	private final Object mLock = new Object();
	
	protected void onSynthesizeText(SynthesisRequest request, SynthesisCallback callback) {
		...
		// 让之前请求的线程释放锁
		isLast = false;
        	// 当前(最后一个)请求线程获取锁
		synchronized (mLock) {
  			isLast = true;
			// 接收到后来线程请求、或所有合成完成时释放锁
			while (isLast && !isDone) {
				Thread.sleep(100);
			}
		}
	}

实在不想折腾就算了,可以理解

@GeminiT369
Copy link
Author

就算暂停后callback.audioAvailable()也是返回 0 SUCCESS,所以说没法知道是否暂停

对,看来这个方法必须阻塞了

@jing332
Copy link
Owner

jing332 commented Dec 8, 2022

onSynthesizeText在return后系统才会调用后续的onSynthesizeText
这行不通吧

@jing332
Copy link
Owner

jing332 commented Dec 8, 2022

算了 不折腾了

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants