AudioSource.java
package com.shengzhiai.yugu.audio;
import com.shengzhiai.yugu.errors.ErrorTable;
import com.shengzhiai.yugu.errors.YuguErrors;
import com.shengzhiai.yugu.errors.YuguException;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.Objects;
/**
* Audio supplied by the caller (requirement C-06): bytes, a file, a path, a stream or raw PCM.
* The SDK reads the audio once per call, so retries upload the same bytes.
*
* <p>Streams are read to the end but not closed; the caller owns them.
*/
public final class AudioSource {
/** Upper bound for audio read into memory, larger inputs are rejected before any I/O. */
public static final long MAX_READ_BYTES = 64L * 1024 * 1024;
private enum Kind { BYTES, FILE, STREAM, PCM }
private final Kind kind;
private final byte[] bytes;
private final Path path;
private final InputStream stream;
private final int sampleRate;
private final int channels;
private final String fileName;
private final String contentType;
private AudioSource(Kind kind, byte[] bytes, Path path, InputStream stream, int sampleRate, int channels,
String fileName, String contentType) {
this.kind = kind;
this.bytes = bytes;
this.path = path;
this.stream = stream;
this.sampleRate = sampleRate;
this.channels = channels;
this.fileName = fileName;
this.contentType = contentType;
}
/**
* @param audio WAV or MP3 bytes
* @return source
*/
public static AudioSource of(byte[] audio) {
return new AudioSource(Kind.BYTES, Objects.requireNonNull(audio, "audio"), null, null, 0, 0, null, null);
}
/**
* @param file WAV or MP3 file
* @return source
*/
public static AudioSource of(File file) {
return of(Objects.requireNonNull(file, "file").toPath());
}
/**
* @param path WAV or MP3 file
* @return source
*/
public static AudioSource of(Path path) {
Objects.requireNonNull(path, "path");
Path name = path.getFileName();
return new AudioSource(Kind.FILE, null, path, null, 0, 0, name == null ? null : name.toString(), null);
}
/**
* @param in stream with WAV or MP3 content; read to the end, not closed
* @return source
*/
public static AudioSource of(InputStream in) {
return new AudioSource(Kind.STREAM, null, null, Objects.requireNonNull(in, "in"), 0, 0, null, null);
}
/**
* Raw 16 bit little endian mono PCM. The SDK wraps it into a WAV container for upload.
*
* @param pcm samples
* @param sampleRate sample rate in Hz, 16000 recommended
* @return source
*/
public static AudioSource pcm(byte[] pcm, int sampleRate) {
return new AudioSource(Kind.PCM, Objects.requireNonNull(pcm, "pcm"), null, null, sampleRate, 1, "audio.wav", "audio/wav");
}
/**
* @param name file name for the multipart part
* @return a copy with the file name
*/
public AudioSource withFileName(String name) {
return new AudioSource(kind, bytes, path, stream, sampleRate, channels, name, contentType);
}
/**
* @param type content type of the multipart part
* @return a copy with the content type
*/
public AudioSource withContentType(String type) {
return new AudioSource(kind, bytes, path, stream, sampleRate, channels, fileName, type);
}
/**
* Reads the audio. Called once per logical call.
*
* @return loaded audio
* @throws YuguException {@code InvalidParameterException} 90010 when the file is missing,
* unreadable, empty or larger than {@link #MAX_READ_BYTES}
*/
public Loaded load() throws YuguException {
byte[] data;
switch (kind) {
case FILE:
try {
if (!Files.isRegularFile(path)) {
throw invalid("audio file not found: " + path, null);
}
if (Files.size(path) > MAX_READ_BYTES) {
throw invalid("audio file larger than " + MAX_READ_BYTES + " bytes: " + path, null);
}
data = Files.readAllBytes(path);
} catch (NoSuchFileException e) {
throw invalid("audio file not found: " + path, e);
} catch (IOException e) {
throw invalid("audio file not readable: " + path + ": " + e.getMessage(), e);
}
break;
case STREAM:
data = readAll(stream);
break;
case PCM:
AudioAnalysis pcmAnalysis = AudioPrecheck.analyzePcm(bytes, sampleRate, channels);
return new Loaded(WavInfo.wrapPcm16(bytes, sampleRate, channels), fileName, contentType,
AudioFormat.PCM, pcmAnalysis);
default:
data = bytes;
}
if (data.length == 0) {
throw invalid("audio is empty", null);
}
AudioFormat f = AudioPrecheck.detect(data);
String name = fileName != null ? fileName : f == AudioFormat.MP3 ? "audio.mp3" : "audio.wav";
String type = contentType != null ? contentType
: f == AudioFormat.WAV ? "audio/wav" : f == AudioFormat.MP3 ? "audio/mpeg" : "application/octet-stream";
return new Loaded(data, name, type, f, null);
}
private static byte[] readAll(InputStream in) {
try {
ByteArrayOutputStream out = new ByteArrayOutputStream(64 * 1024);
byte[] buf = new byte[16 * 1024];
long total = 0;
int n;
while ((n = in.read(buf)) >= 0) {
total += n;
if (total > MAX_READ_BYTES) {
throw invalid("audio stream larger than " + MAX_READ_BYTES + " bytes", null);
}
out.write(buf, 0, n);
}
return out.toByteArray();
} catch (IOException e) {
throw invalid("audio stream not readable: " + e.getMessage(), e);
}
}
private static YuguException invalid(String message, Throwable cause) {
return YuguErrors.local(ErrorTable.INVALID_ARGUMENT, message, cause);
}
/** Audio read into memory, ready for upload. */
public static final class Loaded {
private final byte[] data;
private final String fileName;
private final String contentType;
private final AudioFormat format;
private final AudioAnalysis pcmAnalysis;
Loaded(byte[] data, String fileName, String contentType, AudioFormat format, AudioAnalysis pcmAnalysis) {
this.data = data;
this.fileName = fileName;
this.contentType = contentType;
this.format = format;
this.pcmAnalysis = pcmAnalysis;
}
/** @return bytes to upload */
public byte[] getData() {
return data;
}
/** @return multipart file name */
public String getFileName() {
return fileName;
}
/** @return multipart content type */
public String getContentType() {
return contentType;
}
/** @return format the caller supplied */
public AudioFormat getFormat() {
return format;
}
/** @return precheck analysis of the payload */
public AudioAnalysis analyze() {
return pcmAnalysis != null ? pcmAnalysis : AudioPrecheck.analyze(data);
}
}
}