AudioInput.kt
package com.shengzhiai.yugu
import com.shengzhiai.yugu.audio.WavFormat
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.IOException
import java.io.InputStream
/**
* 整段评测的音频来源,支持字节数组,文件,输入流与裸 PCM。
*
* - [fromBytes],[fromFile],[fromStream]:WAV 或 MP3 等平台支持的格式,按原样上传。
* - [fromPcm]:16 位小端裸 PCM,上传前 SDK 自动加 WAV 头。
*
* 输入流在 [fromStream] 调用时读到结尾,流由调用方关闭。单个音频最多读入 64 MB。
*/
public class AudioInput private constructor(
private val loader: () -> ByteArray,
/** 上传时 `audio` 段的文件名。 */
public val fileName: String,
private val declaredContentType: String?,
internal val pcmSampleRate: Int,
internal val pcmChannels: Int,
internal val isRawPcm: Boolean,
) {
@Volatile
private var cached: ByteArray? = null
/** 读出的原始字节,结果缓存,重试时复用。 */
internal fun bytes(): ByteArray {
cached?.let { return it }
val b = loader()
cached = b
return b
}
/** 上传用字节:裸 PCM 加 WAV 头,其余原样。 */
internal fun uploadBytes(): ByteArray =
if (isRawPcm) WavFormat.fromPcm(bytes(), pcmSampleRate, pcmChannels) else bytes()
/** 上传时 `audio` 段的 Content-Type。 */
internal fun contentType(): String {
declaredContentType?.let { return it }
if (isRawPcm) return "audio/wav"
val b = bytes()
if (WavFormat.isWav(b)) return "audio/wav"
return contentTypeForName(fileName)
}
public companion object {
/** 单个音频读入内存的上限。 */
public const val MAX_READ_BYTES: Int = 64 * 1024 * 1024
/** 字节数组形式的音频,常见为 WAV 或 MP3。 */
@JvmStatic
@JvmOverloads
public fun fromBytes(bytes: ByteArray, fileName: String = "audio.wav", contentType: String? = null): AudioInput {
if (bytes.isEmpty()) throw invalidArgument("音频为空")
return AudioInput({ bytes }, fileName, contentType, 0, 0, false)
}
/** 文件形式的音频,在评测调用时读取。 */
@JvmStatic
@JvmOverloads
public fun fromFile(file: File, contentType: String? = null): AudioInput = AudioInput(
{
if (!file.isFile || !file.canRead()) throw invalidArgument("音频文件不存在或不可读:${file.path}")
if (file.length() > MAX_READ_BYTES) throw invalidArgument("音频文件超过 64 MB:${file.path}")
try {
file.readBytes().also { if (it.isEmpty()) throw invalidArgument("音频文件为空:${file.path}") }
} catch (e: IOException) {
throw invalidArgument("读取音频文件失败:${file.path}", e)
}
},
file.name.ifEmpty { "audio.wav" }, contentType, 0, 0, false,
)
/** 输入流形式的音频,调用时读到结尾,流由调用方关闭。 */
@JvmStatic
@JvmOverloads
public fun fromStream(stream: InputStream, fileName: String = "audio.wav", contentType: String? = null): AudioInput {
val bytes = readAll(stream)
if (bytes.isEmpty()) throw invalidArgument("音频流为空")
return AudioInput({ bytes }, fileName, contentType, 0, 0, false)
}
/** 16 位小端裸 PCM,默认 16 kHz 单声道,上传前加 WAV 头。 */
@JvmStatic
@JvmOverloads
public fun fromPcm(pcm: ByteArray, sampleRate: Int = 16_000, channels: Int = 1): AudioInput {
if (pcm.isEmpty()) throw invalidArgument("PCM 为空")
if (sampleRate <= 0 || channels <= 0) throw invalidArgument("采样率与声道数必须为正数")
return AudioInput({ pcm }, "audio.wav", "audio/wav", sampleRate, channels, true)
}
internal fun readAll(stream: InputStream): ByteArray {
val out = ByteArrayOutputStream()
val buf = ByteArray(16 * 1024)
try {
while (true) {
val n = stream.read(buf)
if (n < 0) break
out.write(buf, 0, n)
if (out.size() > MAX_READ_BYTES) throw invalidArgument("音频流超过 64 MB")
}
} catch (e: IOException) {
throw invalidArgument("读取音频流失败", e)
}
return out.toByteArray()
}
internal fun contentTypeForName(name: String): String = when (name.substringAfterLast('.', "").lowercase()) {
"wav" -> "audio/wav"
"mp3" -> "audio/mpeg"
"m4a", "mp4", "aac" -> "audio/mp4"
"ogg", "opus" -> "audio/ogg"
"amr" -> "audio/amr"
"flac" -> "audio/flac"
else -> "application/octet-stream"
}
}
}
/** 看图说话的图片,仅 coreType 为 open 且 taskType 为 picture 时上传。 */
public class ImageInput @JvmOverloads constructor(
internal val bytes: ByteArray,
public val fileName: String = "image.jpg",
public val contentType: String = "image/jpeg",
) {
public companion object {
/** 从文件读取图片。 */
@JvmStatic
@JvmOverloads
public fun fromFile(file: File, contentType: String = "image/jpeg"): ImageInput {
if (!file.isFile || !file.canRead()) throw invalidArgument("图片文件不存在或不可读:${file.path}")
return try {
ImageInput(file.readBytes(), file.name, contentType)
} catch (e: IOException) {
throw invalidArgument("读取图片失败:${file.path}", e)
}
}
}
}