13 package ai.picovoice.porcupinedemo;
16 import org.apache.commons.cli.*;
18 import javax.sound.sampled.*;
19 import java.io.ByteArrayInputStream;
20 import java.io.ByteArrayOutputStream;
22 import java.io.IOException;
23 import java.nio.ByteBuffer;
24 import java.nio.ByteOrder;
25 import java.time.LocalTime;
26 import java.time.format.DateTimeFormatter;
30 public static void runDemo(String accessKey, String libPath, String modelPath,
31 String[] keywordPaths,
float[] sensitivities,
32 int audioDeviceIndex, String outputPath) {
35 String[]
keywords =
new String[keywordPaths.length];
36 for (
int i = 0; i < keywordPaths.length; i++) {
37 File keywordFile =
new File(keywordPaths[i]);
38 if (!keywordFile.exists())
39 throw new IllegalArgumentException(String.format(
"Keyword file at '%s' " +
40 "does not exist", keywordPaths[i]));
41 keywords[i] = keywordFile.getName().split(
"_")[0];
45 File outputFile =
null;
46 ByteArrayOutputStream outputStream =
null;
47 long totalBytesCaptured = 0;
48 AudioFormat format =
new AudioFormat(16000
f, 16, 1,
true,
false);
51 DataLine.Info dataLineInfo =
new DataLine.Info(TargetDataLine.class, format);
52 TargetDataLine micDataLine;
55 micDataLine.open(format);
56 }
catch (LineUnavailableException e) {
57 System.err.println(
"Failed to get a valid capture device. Use --show_audio_devices to " +
58 "show available capture devices and their indices");
74 if (outputPath !=
null) {
75 outputFile =
new File(outputPath);
76 outputStream =
new ByteArrayOutputStream();
80 System.out.print(
"Listening for {");
81 for (
int i = 0; i <
keywords.length; i++) {
82 System.out.printf(
" %s(%.02f)",
keywords[i], sensitivities[i]);
84 System.out.print(
" }\n");
85 System.out.println(
"Press enter to stop recording...");
89 ByteBuffer captureBuffer = ByteBuffer.allocate(frameLength * 2);
90 captureBuffer.order(ByteOrder.LITTLE_ENDIAN);
91 short[] porcupineBuffer =
new short[frameLength];
94 while (System.in.available() == 0) {
97 numBytesRead = micDataLine.read(captureBuffer.array(), 0, captureBuffer.capacity());
98 totalBytesCaptured += numBytesRead;
101 if (outputStream !=
null) {
102 outputStream.write(captureBuffer.array(), 0, numBytesRead);
106 if (numBytesRead != frameLength * 2) {
111 captureBuffer.asShortBuffer().get(porcupineBuffer);
114 int result = porcupine.
process(porcupineBuffer);
116 System.out.printf(
"[%s] Detected '%s'\n",
117 LocalTime.now().format(DateTimeFormatter.ofPattern(
"HH:mm:ss")),
keywords[result]);
120 }
catch (Exception e) {
121 System.err.println(e.toString());
123 if (outputStream !=
null && outputFile !=
null) {
126 ByteArrayInputStream writeArray =
new ByteArrayInputStream(outputStream.toByteArray());
127 AudioInputStream writeStream =
new AudioInputStream(writeArray, format, totalBytesCaptured / format.getFrameSize());
130 AudioSystem.write(writeStream, AudioFileFormat.Type.WAVE, outputFile);
131 }
catch (IOException e) {
132 System.err.printf(
"Failed to write audio to '%s'.\n", outputFile.getPath());
137 if (porcupine !=
null) {
146 Mixer.Info[] allMixerInfo = AudioSystem.getMixerInfo();
147 Line.Info captureLine =
new Line.Info(TargetDataLine.class);
149 for (
int i = 0; i < allMixerInfo.length; i++) {
152 Mixer mixer = AudioSystem.getMixer(allMixerInfo[i]);
153 if (mixer.isLineSupported(captureLine)) {
154 System.out.printf(
"Device %d: %s\n", i, allMixerInfo[i].
getName());
161 if (!AudioSystem.isLineSupported(dataLineInfo)) {
162 throw new LineUnavailableException(
"Default capture device does not support the audio " +
163 "format required by Picovoice (16kHz, 16-bit, linearly-encoded, single-channel PCM).");
166 return (TargetDataLine) AudioSystem.getLine(dataLineInfo);
169 private static TargetDataLine
getAudioDevice(
int deviceIndex, DataLine.Info dataLineInfo)
throws LineUnavailableException {
171 if (deviceIndex >= 0) {
173 Mixer.Info mixerInfo = AudioSystem.getMixerInfo()[deviceIndex];
174 Mixer mixer = AudioSystem.getMixer(mixerInfo);
176 if (mixer.isLineSupported(dataLineInfo)) {
177 return (TargetDataLine) mixer.getLine(dataLineInfo);
179 System.err.printf(
"Audio capture device at index %s does not support the audio format required by " +
180 "Picovoice. Using default capture device.", deviceIndex);
182 }
catch (Exception e) {
183 System.err.printf(
"No capture device found at index %s. Using default capture device.", deviceIndex);
194 CommandLineParser parser =
new DefaultParser();
195 HelpFormatter formatter =
new HelpFormatter();
199 cmd = parser.parse(options,
args);
200 }
catch (ParseException e) {
201 System.out.println(e.getMessage());
202 formatter.printHelp(
"porcupinemicdemo", options);
207 if (
cmd.hasOption(
"help")) {
208 formatter.printHelp(
"porcupinemicdemo", options);
212 if (
cmd.hasOption(
"show_audio_devices")) {
217 String accessKey =
cmd.getOptionValue(
"access_key");
218 String libraryPath =
cmd.getOptionValue(
"library_path");
219 String modelPath =
cmd.getOptionValue(
"model_path");
220 String[]
keywords =
cmd.getOptionValues(
"keywords");
221 String[] keywordPaths =
cmd.getOptionValues(
"keyword_paths");
222 String[] sensitivitiesStr =
cmd.getOptionValues(
"sensitivities");
223 String audioDeviceIndexStr =
cmd.getOptionValue(
"audio_device_index");
224 String outputPath =
cmd.getOptionValue(
"output_path");
226 if (accessKey ==
null || accessKey.length() == 0) {
227 throw new IllegalArgumentException(
"AccessKey is required for Porcupine.");
231 float[] sensitivities =
null;
232 if (sensitivitiesStr !=
null) {
233 sensitivities =
new float[sensitivitiesStr.length];
235 for (
int i = 0; i < sensitivitiesStr.length; i++) {
238 sensitivity = Float.parseFloat(sensitivitiesStr[i]);
239 }
catch (Exception e) {
240 throw new IllegalArgumentException(
"Failed to parse sensitivity value. " +
241 "Must be a decimal value between [0,1].");
244 if (sensitivity < 0 || sensitivity > 1) {
245 throw new IllegalArgumentException(String.format(
"Failed to parse sensitivity value (%s). " +
246 "Must be a decimal value between [0,1].", sensitivitiesStr[i]));
248 sensitivities[i] = sensitivity;
252 if (libraryPath ==
null) {
256 if (modelPath ==
null) {
260 if (keywordPaths ==
null || keywordPaths.length == 0) {
262 throw new IllegalArgumentException(
"Either '--keywords' or '--keyword_paths' must be set.");
265 keywordPaths =
new String[
keywords.length];
266 for (
int i = 0; i <
keywords.length; i++) {
267 final String keyword =
keywords[i].toUpperCase().replace(
" ",
"_");
271 }
catch (Exception e) {
272 throw new IllegalArgumentException(String.format(
"'%s' not a built-in keyword", keyword));
277 if (sensitivities ==
null) {
278 sensitivities =
new float[keywordPaths.length];
279 Arrays.fill(sensitivities, 0.5
f);
282 if (sensitivities.length != keywordPaths.length) {
283 throw new IllegalArgumentException(String.format(
"Number of keywords (%d) does " +
284 "not match number of sensitivities (%d)", keywordPaths.length, sensitivities.length));
287 int audioDeviceIndex = -1;
288 if (audioDeviceIndexStr !=
null) {
290 audioDeviceIndex = Integer.parseInt(audioDeviceIndexStr);
291 if (audioDeviceIndex < 0) {
292 throw new IllegalArgumentException(String.format(
"Audio device index %s is not a " +
293 "valid positive integer.", audioDeviceIndexStr));
295 }
catch (Exception e) {
296 throw new IllegalArgumentException(String.format(
"Audio device index '%s' is not a " +
297 "valid positive integer.", audioDeviceIndexStr));
301 runDemo(accessKey, libraryPath, modelPath, keywordPaths, sensitivities, audioDeviceIndex, outputPath);
305 Options options =
new Options();
307 options.addOption(Option.builder(
"a")
308 .longOpt(
"access_key")
310 .desc(
"AccessKey obtained from Picovoice Console (https://picovoice.ai/console/).")
313 options.addOption(Option.builder(
"l")
314 .longOpt(
"library_path")
316 .desc(
"Absolute path to the Porcupine native runtime library.")
319 options.addOption(Option.builder(
"m")
320 .longOpt(
"model_path")
322 .desc(
"Absolute path to the file containing model parameters.")
325 options.addOption(Option.builder(
"k")
331 options.addOption(Option.builder(
"kp")
332 .longOpt(
"keyword_paths")
334 .desc(
"Absolute paths to keyword model files.")
337 options.addOption(Option.builder(
"s")
338 .longOpt(
"sensitivities")
340 .desc(
"Sensitivities for detecting keywords. Each value should be a number within [0, 1]. A higher " +
341 "sensitivity results in fewer misses at the cost of increasing the false alarm rate. " +
342 "If not set 0.5 will be used.")
345 options.addOption(Option.builder(
"o")
346 .longOpt(
"output_path")
348 .desc(
"Absolute path to recorded audio for debugging.")
351 options.addOption(Option.builder(
"di")
352 .longOpt(
"audio_device_index")
354 .desc(
"Index of input audio device.")
357 options.addOption(
new Option(
"sd",
"show_audio_devices",
false,
"Print available recording devices."));
358 options.addOption(
new Option(
"h",
"help",
false,
""));