1 changed files with 209 additions and 0 deletions
@ -0,0 +1,209 @@ |
|||
package com.yunqiinnovation.azure_speech |
|||
|
|||
import android.content.Context |
|||
import android.util.Base64 |
|||
import android.util.Log |
|||
import okhttp3.OkHttpClient |
|||
import okhttp3.Request |
|||
import okhttp3.WebSocket |
|||
import okhttp3.WebSocketListener |
|||
import okio.ByteString |
|||
import org.json.JSONObject |
|||
import java.io.File |
|||
import java.io.FileInputStream |
|||
import java.net.URLEncoder |
|||
import java.nio.charset.StandardCharsets |
|||
import java.text.SimpleDateFormat |
|||
import java.util.Date |
|||
import java.util.Locale |
|||
import java.util.TimeZone |
|||
import java.util.UUID |
|||
import java.util.concurrent.CountDownLatch |
|||
import java.util.concurrent.TimeUnit |
|||
import java.util.concurrent.atomic.AtomicBoolean |
|||
import javax.crypto.Mac |
|||
import javax.crypto.spec.SecretKeySpec |
|||
|
|||
class XunFeiAsrHelper(private val context: Context) { |
|||
private val tag = "XunFeiAsrHelper" |
|||
|
|||
private val appId = "43d87d22" |
|||
private val accessKeyId = "2bd01ce06ac35957fc0c082f9b6284ec" |
|||
|
|||
private val audioEncode = "pcm_s16le" |
|||
private val lang = "autodialect" |
|||
private val sampleRate = "16000" |
|||
private val audioFrameSize = 1280 |
|||
private val frameIntervalMs = 40 |
|||
private val baseWsUrl = "wss://office-api-ast-dx.iflyaisol.com/ast/communicate/v1" |
|||
private val client = OkHttpClient.Builder().retryOnConnectionFailure(true).build() |
|||
|
|||
private var webSocket: WebSocket? = null |
|||
private val isConnected = AtomicBoolean(false) |
|||
private val isSendingAudio = AtomicBoolean(false) |
|||
private var sessionId: String? = null |
|||
private var audioFileSize: Long = 0 |
|||
|
|||
private fun getAudioFileSize(audioPath: String): Long { |
|||
val file = File(audioPath) |
|||
return if (file.exists() && file.isFile) file.length() else 0L |
|||
} |
|||
|
|||
private fun getUtcTime(): String { |
|||
val sdf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US) |
|||
sdf.timeZone = TimeZone.getTimeZone("GMT+8") |
|||
return sdf.format(Date()) |
|||
} |
|||
|
|||
private fun calculateSignature(params: Map<String, String>, accessKeySecret: String): String { |
|||
val baseStr = buildString { |
|||
var first = true |
|||
for ((key, value) in params.toSortedMap()) { |
|||
if (key == "signature") continue |
|||
if (value.isBlank()) continue |
|||
if (!first) append("&") |
|||
append(URLEncoder.encode(key, StandardCharsets.UTF_8.name())) |
|||
append("=") |
|||
append(URLEncoder.encode(value, StandardCharsets.UTF_8.name())) |
|||
first = false |
|||
} |
|||
} |
|||
val mac = Mac.getInstance("HmacSHA1") |
|||
val keySpec = SecretKeySpec(accessKeySecret.toByteArray(StandardCharsets.UTF_8), "HmacSHA1") |
|||
mac.init(keySpec) |
|||
val signBytes = mac.doFinal(baseStr.toByteArray(StandardCharsets.UTF_8)) |
|||
return Base64.encodeToString(signBytes, Base64.NO_WRAP) |
|||
} |
|||
|
|||
private fun buildParamsString(params: Map<String, String>): String { |
|||
val sb = StringBuilder() |
|||
var first = true |
|||
for ((key, value) in params) { |
|||
if (!first) sb.append("&") |
|||
sb.append(URLEncoder.encode(key, StandardCharsets.UTF_8.name())) |
|||
sb.append("=") |
|||
sb.append(URLEncoder.encode(value, StandardCharsets.UTF_8.name())) |
|||
first = false |
|||
} |
|||
return sb.toString() |
|||
} |
|||
|
|||
fun connect(appId: String, accessKeyId: String, accessKeySecret: String): Boolean { |
|||
return try { |
|||
val params = mutableMapOf<String, String>() |
|||
params["audio_encode"] = audioEncode |
|||
params["lang"] = lang |
|||
params["samplerate"] = sampleRate |
|||
params["accessKeyId"] = accessKeyId |
|||
params["appId"] = appId |
|||
params["uuid"] = UUID.randomUUID().toString().replace("-", "") |
|||
params["utc"] = getUtcTime() |
|||
params["signature"] = calculateSignature(params, accessKeySecret) |
|||
val fullUrl = "$baseWsUrl?${buildParamsString(params)}" |
|||
|
|||
val request = Request.Builder().url(fullUrl).build() |
|||
val latch = CountDownLatch(1) |
|||
val listener = object : WebSocketListener() { |
|||
override fun onOpen(ws: WebSocket, response: okhttp3.Response) { |
|||
webSocket = ws |
|||
isConnected.set(true) |
|||
latch.countDown() |
|||
} |
|||
|
|||
override fun onMessage(ws: WebSocket, text: String) { |
|||
try { |
|||
val json = JSONObject(text) |
|||
if (json.optString("msg_type") == "action") { |
|||
val data = json.optJSONObject("data") |
|||
val sid = data?.optString("sessionId") |
|||
if (!sid.isNullOrEmpty()) { |
|||
sessionId = sid |
|||
} |
|||
} |
|||
} catch (e: Exception) { |
|||
Log.d(tag, "onMessage parse error: ${e.message}") |
|||
} |
|||
} |
|||
|
|||
override fun onClosed(ws: WebSocket, code: Int, reason: String) { |
|||
isConnected.set(false) |
|||
} |
|||
|
|||
override fun onFailure(ws: WebSocket, t: Throwable, response: okhttp3.Response?) { |
|||
isConnected.set(false) |
|||
Log.e(tag, "WebSocket failure: ${t.message}") |
|||
} |
|||
} |
|||
client.newWebSocket(request, listener) |
|||
latch.await(15, TimeUnit.SECONDS) |
|||
isConnected.get() |
|||
} catch (e: Exception) { |
|||
Log.e(tag, "connect failed: ${e.message}") |
|||
false |
|||
} |
|||
} |
|||
|
|||
fun sendAudio(audioPath: String): Boolean { |
|||
if (!isConnected.get() || webSocket == null || isSendingAudio.get()) { |
|||
Log.d(tag, "sendAudio invalid state") |
|||
return false |
|||
} |
|||
isSendingAudio.set(true) |
|||
audioFileSize = getAudioFileSize(audioPath) |
|||
return try { |
|||
FileInputStream(audioPath).use { fis -> |
|||
val buffer = ByteArray(audioFrameSize) |
|||
var frameIndex = 0 |
|||
var startTime: Long? = null |
|||
while (true) { |
|||
val bytesRead = fis.read(buffer) |
|||
if (bytesRead == -1) break |
|||
val frameData = if (bytesRead == audioFrameSize) buffer else buffer.copyOf(bytesRead) |
|||
if (startTime == null) startTime = System.currentTimeMillis() |
|||
val expectedSendTime = startTime!! + frameIndex * frameIntervalMs |
|||
val now = System.currentTimeMillis() |
|||
val diff = expectedSendTime - now |
|||
if (diff > 1) Thread.sleep(diff) |
|||
webSocket?.send(ByteString.of(frameData, 0, frameData.size)) |
|||
frameIndex++ |
|||
} |
|||
val endMsg = JSONObject() |
|||
endMsg.put("end", true) |
|||
sessionId?.let { endMsg.put("sessionId", it) } |
|||
webSocket?.send(endMsg.toString()) |
|||
true |
|||
} |
|||
} catch (e: Exception) { |
|||
Log.e(tag, "sendAudio error: ${e.message}") |
|||
false |
|||
} finally { |
|||
isSendingAudio.set(false) |
|||
} |
|||
} |
|||
|
|||
fun runDemo(appId: String, accessKeyId: String, accessKeySecret: String, audioPath: String) { |
|||
try { |
|||
if (!connect(appId, accessKeyId, accessKeySecret)) return |
|||
if (!sendAudio(audioPath)) return |
|||
val estimatedDurationSec = (((audioFileSize / audioFrameSize) * frameIntervalMs) / 1000).toInt() |
|||
val waitTime = estimatedDurationSec + 5 |
|||
for (i in 0..waitTime) { |
|||
if (!isConnected.get()) break |
|||
Thread.sleep(1000) |
|||
} |
|||
} catch (_: InterruptedException) { |
|||
} finally { |
|||
close() |
|||
} |
|||
} |
|||
|
|||
fun close() { |
|||
try { |
|||
isConnected.set(false) |
|||
webSocket?.close(1000, "normal") |
|||
} catch (_: Exception) { |
|||
} finally { |
|||
webSocket = null |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue