1 /*
2 * Copyright 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 #include <fstream>
18 #include <iostream>
19 #include <vector>
20
21 #include "util/WaveFileWriter.h"
22
23 #include "NativeAudioContext.h"
24
25 using namespace oboe;
26
convertNativeApiToAudioApi(int nativeApi)27 static oboe::AudioApi convertNativeApiToAudioApi(int nativeApi) {
28 switch (nativeApi) {
29 default:
30 case NATIVE_MODE_UNSPECIFIED:
31 return oboe::AudioApi::Unspecified;
32 case NATIVE_MODE_AAUDIO:
33 return oboe::AudioApi::AAudio;
34 case NATIVE_MODE_OPENSLES:
35 return oboe::AudioApi::OpenSLES;
36 }
37 }
38
39 class MyOboeOutputStream : public WaveFileOutputStream {
40 public:
write(uint8_t b)41 void write(uint8_t b) override {
42 mData.push_back(b);
43 }
44
length()45 int32_t length() {
46 return (int32_t) mData.size();
47 }
48
getData()49 uint8_t *getData() {
50 return mData.data();
51 }
52
53 private:
54 std::vector<uint8_t> mData;
55 };
56
57 bool ActivityContext::mUseCallback = true;
58 int ActivityContext::callbackSize = 0;
59
getOutputStream()60 oboe::AudioStream * ActivityContext::getOutputStream() {
61 for (auto entry : mOboeStreams) {
62 oboe::AudioStream *oboeStream = entry.second.get();
63 if (oboeStream->getDirection() == oboe::Direction::Output) {
64 return oboeStream;
65 }
66 }
67 return nullptr;
68 }
69
getInputStream()70 oboe::AudioStream * ActivityContext::getInputStream() {
71 for (auto entry : mOboeStreams) {
72 oboe::AudioStream *oboeStream = entry.second.get();
73 if (oboeStream != nullptr) {
74 if (oboeStream->getDirection() == oboe::Direction::Input) {
75 return oboeStream;
76 }
77 }
78 }
79 return nullptr;
80 }
81
freeStreamIndex(int32_t streamIndex)82 void ActivityContext::freeStreamIndex(int32_t streamIndex) {
83 mOboeStreams[streamIndex].reset();
84 mOboeStreams.erase(streamIndex);
85 }
86
allocateStreamIndex()87 int32_t ActivityContext::allocateStreamIndex() {
88 return mNextStreamHandle++;
89 }
90
close(int32_t streamIndex)91 void ActivityContext::close(int32_t streamIndex) {
92 stopBlockingIOThread();
93 oboe::AudioStream *oboeStream = getStream(streamIndex);
94 if (oboeStream != nullptr) {
95 oboeStream->close();
96 LOGD("ActivityContext::%s() delete stream %d ", __func__, streamIndex);
97 freeStreamIndex(streamIndex);
98 }
99 }
100
isMMapUsed(int32_t streamIndex)101 bool ActivityContext::isMMapUsed(int32_t streamIndex) {
102 oboe::AudioStream *oboeStream = getStream(streamIndex);
103 if (oboeStream == nullptr) return false;
104 if (oboeStream->getAudioApi() != AudioApi::AAudio) return false;
105 return AAudioExtensions::getInstance().isMMapUsed(oboeStream);
106 }
107
pause()108 oboe::Result ActivityContext::pause() {
109 oboe::Result result = oboe::Result::OK;
110 stopBlockingIOThread();
111 for (auto entry : mOboeStreams) {
112 oboe::AudioStream *oboeStream = entry.second.get();
113 result = oboeStream->requestPause();
114 }
115 return result;
116 }
117
stopAllStreams()118 oboe::Result ActivityContext::stopAllStreams() {
119 oboe::Result result = oboe::Result::OK;
120 stopBlockingIOThread();
121 for (auto entry : mOboeStreams) {
122 oboe::AudioStream *oboeStream = entry.second.get();
123 result = oboeStream->requestStop();
124 }
125 return result;
126 }
127
configureBuilder(bool isInput,oboe::AudioStreamBuilder & builder)128 void ActivityContext::configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) {
129 // We needed the proxy because we did not know the channelCount when we setup the Builder.
130 if (mUseCallback) {
131 LOGD("ActivityContext::open() set callback to use oboeCallbackProxy, callback size = %d",
132 callbackSize);
133 builder.setCallback(&oboeCallbackProxy);
134 builder.setFramesPerCallback(callbackSize);
135 }
136 }
137
open(jint nativeApi,jint sampleRate,jint channelCount,jint format,jint sharingMode,jint performanceMode,jint inputPreset,jint deviceId,jint sessionId,jint framesPerBurst,jboolean channelConversionAllowed,jboolean formatConversionAllowed,jint rateConversionQuality,jboolean isMMap,jboolean isInput)138 int ActivityContext::open(jint nativeApi,
139 jint sampleRate,
140 jint channelCount,
141 jint format,
142 jint sharingMode,
143 jint performanceMode,
144 jint inputPreset,
145 jint deviceId,
146 jint sessionId,
147 jint framesPerBurst,
148 jboolean channelConversionAllowed,
149 jboolean formatConversionAllowed,
150 jint rateConversionQuality,
151 jboolean isMMap,
152 jboolean isInput) {
153
154 oboe::AudioApi audioApi = oboe::AudioApi::Unspecified;
155 switch (nativeApi) {
156 case NATIVE_MODE_UNSPECIFIED:
157 case NATIVE_MODE_AAUDIO:
158 case NATIVE_MODE_OPENSLES:
159 audioApi = convertNativeApiToAudioApi(nativeApi);
160 break;
161 default:
162 return (jint) oboe::Result::ErrorOutOfRange;
163 }
164
165 int32_t streamIndex = allocateStreamIndex();
166 if (streamIndex < 0) {
167 LOGE("ActivityContext::open() stream array full");
168 return (jint) oboe::Result::ErrorNoFreeHandles;
169 }
170
171 if (channelCount < 0 || channelCount > 256) {
172 LOGE("ActivityContext::open() channels out of range");
173 return (jint) oboe::Result::ErrorOutOfRange;
174 }
175
176 // Create an audio stream.
177 oboe::AudioStreamBuilder builder;
178 builder.setChannelCount(channelCount)
179 ->setDirection(isInput ? oboe::Direction::Input : oboe::Direction::Output)
180 ->setSharingMode((oboe::SharingMode) sharingMode)
181 ->setPerformanceMode((oboe::PerformanceMode) performanceMode)
182 ->setInputPreset((oboe::InputPreset)inputPreset)
183 ->setDeviceId(deviceId)
184 ->setSessionId((oboe::SessionId) sessionId)
185 ->setSampleRate(sampleRate)
186 ->setFormat((oboe::AudioFormat) format)
187 ->setChannelConversionAllowed(channelConversionAllowed)
188 ->setFormatConversionAllowed(formatConversionAllowed)
189 ->setSampleRateConversionQuality((oboe::SampleRateConversionQuality) rateConversionQuality)
190 ;
191
192 configureBuilder(isInput, builder);
193
194 builder.setAudioApi(audioApi);
195
196 // Temporarily set the AAudio MMAP policy to disable MMAP or not.
197 bool oldMMapEnabled = AAudioExtensions::getInstance().isMMapEnabled();
198 AAudioExtensions::getInstance().setMMapEnabled(isMMap);
199
200 // Open a stream based on the builder settings.
201 std::shared_ptr<oboe::AudioStream> oboeStream;
202 Result result = builder.openStream(oboeStream);
203 AAudioExtensions::getInstance().setMMapEnabled(oldMMapEnabled);
204 if (result != Result::OK) {
205 freeStreamIndex(streamIndex);
206 streamIndex = -1;
207 } else {
208 mOboeStreams[streamIndex] = oboeStream; // save shared_ptr
209
210 mChannelCount = oboeStream->getChannelCount(); // FIXME store per stream
211 mFramesPerBurst = oboeStream->getFramesPerBurst();
212 mSampleRate = oboeStream->getSampleRate();
213
214 createRecording();
215
216 finishOpen(isInput, oboeStream.get());
217 }
218
219 if (!mUseCallback) {
220 int numSamples = getFramesPerBlock() * mChannelCount;
221 dataBuffer = std::make_unique<float[]>(numSamples);
222 }
223
224 return (result != Result::OK) ? (int)result : streamIndex;
225 }
226
start()227 oboe::Result ActivityContext::start() {
228 oboe::Result result = oboe::Result::OK;
229 oboe::AudioStream *inputStream = getInputStream();
230 oboe::AudioStream *outputStream = getOutputStream();
231 if (inputStream == nullptr && outputStream == nullptr) {
232 LOGD("%s() - no streams defined", __func__);
233 return oboe::Result::ErrorInvalidState; // not open
234 }
235
236 configureForStart();
237
238 audioStreamGateway.reset();
239 result = startStreams();
240
241 if (!mUseCallback && result == oboe::Result::OK) {
242 // Instead of using the callback, start a thread that writes the stream.
243 threadEnabled.store(true);
244 dataThread = new std::thread(threadCallback, this);
245 }
246
247 return result;
248 }
249
saveWaveFile(const char * filename)250 int32_t ActivityContext::saveWaveFile(const char *filename) {
251 if (mRecording == nullptr) {
252 LOGW("ActivityContext::saveWaveFile(%s) but no recording!", filename);
253 return -1;
254 }
255 if (mRecording->getSizeInFrames() == 0) {
256 LOGW("ActivityContext::saveWaveFile(%s) but no frames!", filename);
257 return -2;
258 }
259 MyOboeOutputStream outStream;
260 WaveFileWriter writer(&outStream);
261
262 writer.setFrameRate(mSampleRate);
263 writer.setSamplesPerFrame(mRecording->getChannelCount());
264 writer.setBitsPerSample(24);
265 float buffer[mRecording->getChannelCount()];
266 // Read samples from start to finish.
267 mRecording->rewind();
268 for (int32_t frameIndex = 0; frameIndex < mRecording->getSizeInFrames(); frameIndex++) {
269 mRecording->read(buffer, 1 /* numFrames */);
270 for (int32_t i = 0; i < mRecording->getChannelCount(); i++) {
271 writer.write(buffer[i]);
272 }
273 }
274 writer.close();
275
276 if (outStream.length() > 0) {
277 auto myfile = std::ofstream(filename, std::ios::out | std::ios::binary);
278 myfile.write((char *) outStream.getData(), outStream.length());
279 myfile.close();
280 }
281
282 return outStream.length();
283 }
284
285 // =================================================================== ActivityTestOutput
close(int32_t streamIndex)286 void ActivityTestOutput::close(int32_t streamIndex) {
287 ActivityContext::close(streamIndex);
288 manyToMulti.reset(nullptr);
289 monoToMulti.reset(nullptr);
290 mSinkFloat.reset();
291 mSinkI16.reset();
292 }
293
setChannelEnabled(int channelIndex,bool enabled)294 void ActivityTestOutput::setChannelEnabled(int channelIndex, bool enabled) {
295 if (manyToMulti == nullptr) {
296 return;
297 }
298 if (enabled) {
299 switch (mSignalType) {
300 case SignalType::Sine:
301 sineOscillators[channelIndex].frequency.disconnect();
302 sineOscillators[channelIndex].output.connect(manyToMulti->inputs[channelIndex].get());
303 break;
304 case SignalType::Sawtooth:
305 sawtoothOscillators[channelIndex].output.connect(manyToMulti->inputs[channelIndex].get());
306 break;
307 case SignalType::FreqSweep:
308 mLinearShape.output.connect(&sineOscillators[channelIndex].frequency);
309 sineOscillators[channelIndex].output.connect(manyToMulti->inputs[channelIndex].get());
310 break;
311 case SignalType::PitchSweep:
312 mExponentialShape.output.connect(&sineOscillators[channelIndex].frequency);
313 sineOscillators[channelIndex].output.connect(manyToMulti->inputs[channelIndex].get());
314 break;
315 default:
316 break;
317 }
318 } else {
319 manyToMulti->inputs[channelIndex]->disconnect();
320 }
321 }
322
configureForStart()323 void ActivityTestOutput::configureForStart() {
324 manyToMulti = std::make_unique<ManyToMultiConverter>(mChannelCount);
325
326 mSinkFloat = std::make_unique<SinkFloat>(mChannelCount);
327 mSinkI16 = std::make_unique<SinkI16>(mChannelCount);
328
329 oboe::AudioStream *outputStream = getOutputStream();
330
331 mTriangleOscillator.setSampleRate(outputStream->getSampleRate());
332 mTriangleOscillator.frequency.setValue(1.0/kSweepPeriod);
333 mTriangleOscillator.amplitude.setValue(1.0);
334 mTriangleOscillator.setPhase(-1.0);
335
336 mLinearShape.setMinimum(0.0);
337 mLinearShape.setMaximum(outputStream->getSampleRate() * 0.5); // Nyquist
338
339 mExponentialShape.setMinimum(110.0);
340 mExponentialShape.setMaximum(outputStream->getSampleRate() * 0.5); // Nyquist
341
342 mTriangleOscillator.output.connect(&(mLinearShape.input));
343 mTriangleOscillator.output.connect(&(mExponentialShape.input));
344 {
345 double frequency = 330.0;
346 for (int i = 0; i < mChannelCount; i++) {
347 sineOscillators[i].setSampleRate(outputStream->getSampleRate());
348 sineOscillators[i].frequency.setValue(frequency);
349 frequency *= 4.0 / 3.0; // each sine is at a higher frequency
350 sineOscillators[i].amplitude.setValue(AMPLITUDE_SINE);
351 setChannelEnabled(i, true);
352 }
353 }
354
355 manyToMulti->output.connect(&(mSinkFloat.get()->input));
356 manyToMulti->output.connect(&(mSinkI16.get()->input));
357
358 mSinkFloat->pullReset();
359 mSinkI16->pullReset();
360
361 configureStreamGateway();
362 }
363
configureStreamGateway()364 void ActivityTestOutput::configureStreamGateway() {
365 oboe::AudioStream *outputStream = getOutputStream();
366 if (outputStream->getFormat() == oboe::AudioFormat::I16) {
367 audioStreamGateway.setAudioSink(mSinkI16);
368 } else if (outputStream->getFormat() == oboe::AudioFormat::Float) {
369 audioStreamGateway.setAudioSink(mSinkFloat);
370 }
371
372 if (mUseCallback) {
373 oboeCallbackProxy.setCallback(&audioStreamGateway);
374 }
375 }
376
runBlockingIO()377 void ActivityTestOutput::runBlockingIO() {
378 int32_t framesPerBlock = getFramesPerBlock();
379 oboe::DataCallbackResult callbackResult = oboe::DataCallbackResult::Continue;
380
381 oboe::AudioStream *oboeStream = getOutputStream();
382 if (oboeStream == nullptr) {
383 LOGE("%s() : no stream found\n", __func__);
384 return;
385 }
386
387 while (threadEnabled.load()
388 && callbackResult == oboe::DataCallbackResult::Continue) {
389 // generate output by calling the callback
390 callbackResult = audioStreamGateway.onAudioReady(oboeStream,
391 dataBuffer.get(),
392 framesPerBlock);
393
394 auto result = oboeStream->write(dataBuffer.get(),
395 framesPerBlock,
396 NANOS_PER_SECOND);
397
398 if (!result) {
399 LOGE("%s() returned %s\n", __func__, convertToText(result.error()));
400 break;
401 }
402 int32_t framesWritten = result.value();
403 if (framesWritten < framesPerBlock) {
404 LOGE("%s() : write() wrote %d of %d\n", __func__, framesWritten, framesPerBlock);
405 break;
406 }
407 }
408 }
409
410 // ======================================================================= ActivityTestInput
configureForStart()411 void ActivityTestInput::configureForStart() {
412 mInputAnalyzer.reset();
413 if (mUseCallback) {
414 oboeCallbackProxy.setCallback(&mInputAnalyzer);
415 }
416 mInputAnalyzer.setRecording(mRecording.get());
417 }
418
runBlockingIO()419 void ActivityTestInput::runBlockingIO() {
420 int32_t framesPerBlock = getFramesPerBlock();
421 oboe::DataCallbackResult callbackResult = oboe::DataCallbackResult::Continue;
422
423 oboe::AudioStream *oboeStream = getInputStream();
424 if (oboeStream == nullptr) {
425 LOGE("%s() : no stream found\n", __func__);
426 return;
427 }
428
429 while (threadEnabled.load()
430 && callbackResult == oboe::DataCallbackResult::Continue) {
431
432 // Avoid glitches by waiting until there is extra data in the FIFO.
433 auto err = oboeStream->waitForAvailableFrames(mMinimumFramesBeforeRead, kNanosPerSecond);
434 if (!err) break;
435
436 // read from input
437 auto result = oboeStream->read(dataBuffer.get(),
438 framesPerBlock,
439 NANOS_PER_SECOND);
440 if (!result) {
441 LOGE("%s() : read() returned %s\n", __func__, convertToText(result.error()));
442 break;
443 }
444 int32_t framesRead = result.value();
445 if (framesRead < framesPerBlock) { // timeout?
446 LOGE("%s() : read() read %d of %d\n", __func__, framesRead, framesPerBlock);
447 break;
448 }
449
450 // analyze input
451 callbackResult = mInputAnalyzer.onAudioReady(oboeStream,
452 dataBuffer.get(),
453 framesRead);
454 }
455 }
456
stopPlayback()457 oboe::Result ActivityRecording::stopPlayback() {
458 oboe::Result result = oboe::Result::OK;
459 if (playbackStream != nullptr) {
460 result = playbackStream->requestStop();
461 playbackStream->close();
462 mPlayRecordingCallback.setRecording(nullptr);
463 delete playbackStream;
464 playbackStream = nullptr;
465 }
466 return result;
467 }
468
startPlayback()469 oboe::Result ActivityRecording::startPlayback() {
470 stop();
471 oboe::AudioStreamBuilder builder;
472 builder.setChannelCount(mChannelCount)
473 ->setSampleRate(mSampleRate)
474 ->setFormat(oboe::AudioFormat::Float)
475 ->setCallback(&mPlayRecordingCallback)
476 ->setAudioApi(oboe::AudioApi::OpenSLES);
477 oboe::Result result = builder.openStream(&playbackStream);
478 if (result != oboe::Result::OK) {
479 delete playbackStream;
480 playbackStream = nullptr;
481 } else if (playbackStream != nullptr) {
482 if (mRecording != nullptr) {
483 mRecording->rewind();
484 mPlayRecordingCallback.setRecording(mRecording.get());
485 result = playbackStream->requestStart();
486 }
487 }
488 return result;
489 }
490
491 // ======================================================================= ActivityTapToTone
configureForStart()492 void ActivityTapToTone::configureForStart() {
493 monoToMulti = std::make_unique<MonoToMultiConverter>(mChannelCount);
494
495 mSinkFloat = std::make_unique<SinkFloat>(mChannelCount);
496 mSinkI16 = std::make_unique<SinkI16>(mChannelCount);
497
498 oboe::AudioStream *outputStream = getOutputStream();
499 sawPingGenerator.setSampleRate(outputStream->getSampleRate());
500 sawPingGenerator.frequency.setValue(FREQUENCY_SAW_PING);
501 sawPingGenerator.amplitude.setValue(AMPLITUDE_SAW_PING);
502
503 sawPingGenerator.output.connect(&(monoToMulti->input));
504 monoToMulti->output.connect(&(mSinkFloat.get()->input));
505 monoToMulti->output.connect(&(mSinkI16.get()->input));
506
507 mSinkFloat->pullReset();
508 mSinkI16->pullReset();
509
510 sawPingGenerator.setEnabled(false);
511 configureStreamGateway();
512 }
513
514 // ======================================================================= ActivityRoundTripLatency
configureBuilder(bool isInput,oboe::AudioStreamBuilder & builder)515 void ActivityFullDuplex::configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) {
516 if (isInput) {
517 // Ideally the output streams should be opened first.
518 oboe::AudioStream *outputStream = getOutputStream();
519 if (outputStream != nullptr) {
520 // Make sure the capacity is bigger than two bursts.
521 int32_t burst = outputStream->getFramesPerBurst();
522 builder.setBufferCapacityInFrames(2 * burst);
523 }
524 }
525 }
526
527 // ======================================================================= ActivityEcho
configureBuilder(bool isInput,oboe::AudioStreamBuilder & builder)528 void ActivityEcho::configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) {
529 ActivityFullDuplex::configureBuilder(isInput, builder);
530
531 if (mFullDuplexEcho.get() == nullptr) {
532 mFullDuplexEcho = std::make_unique<FullDuplexEcho>();
533 }
534 // only output uses a callback, input is polled
535 if (!isInput) {
536 builder.setCallback(mFullDuplexEcho.get());
537 }
538 }
539
finishOpen(bool isInput,oboe::AudioStream * oboeStream)540 void ActivityEcho::finishOpen(bool isInput, oboe::AudioStream *oboeStream) {
541 if (isInput) {
542 mFullDuplexEcho->setInputStream(oboeStream);
543 } else {
544 mFullDuplexEcho->setOutputStream(oboeStream);
545 }
546 }
547
548 // ======================================================================= ActivityRoundTripLatency
configureBuilder(bool isInput,oboe::AudioStreamBuilder & builder)549 void ActivityRoundTripLatency::configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) {
550 ActivityFullDuplex::configureBuilder(isInput, builder);
551
552 if (mFullDuplexLatency.get() == nullptr) {
553 mFullDuplexLatency = std::make_unique<FullDuplexLatency>();
554 }
555 if (!isInput) {
556 // only output uses a callback, input is polled
557 builder.setCallback(mFullDuplexLatency.get());
558 }
559 }
560
finishOpen(bool isInput,oboe::AudioStream * oboeStream)561 void ActivityRoundTripLatency::finishOpen(bool isInput, oboe::AudioStream *oboeStream) {
562 if (isInput) {
563 mFullDuplexLatency->setInputStream(oboeStream);
564 mFullDuplexLatency->setRecording(mRecording.get());
565 } else {
566 mFullDuplexLatency->setOutputStream(oboeStream);
567 }
568 }
569
570 // ======================================================================= ActivityGlitches
configureBuilder(bool isInput,oboe::AudioStreamBuilder & builder)571 void ActivityGlitches::configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) {
572 ActivityFullDuplex::configureBuilder(isInput, builder);
573
574 if (mFullDuplexGlitches.get() == nullptr) {
575 mFullDuplexGlitches = std::make_unique<FullDuplexGlitches>();
576 }
577 if (!isInput) {
578 // only output uses a callback, input is polled
579 builder.setCallback(mFullDuplexGlitches.get());
580 }
581 }
582
finishOpen(bool isInput,oboe::AudioStream * oboeStream)583 void ActivityGlitches::finishOpen(bool isInput, oboe::AudioStream *oboeStream) {
584 if (isInput) {
585 mFullDuplexGlitches->setInputStream(oboeStream);
586 mFullDuplexGlitches->setRecording(mRecording.get());
587 } else {
588 mFullDuplexGlitches->setOutputStream(oboeStream);
589 }
590 }
591
592
593 // =================================================================== ActivityTestDisconnect
close(int32_t streamIndex)594 void ActivityTestDisconnect::close(int32_t streamIndex) {
595 ActivityContext::close(streamIndex);
596 mSinkFloat.reset();
597 }
598
configureForStart()599 void ActivityTestDisconnect::configureForStart() {
600 oboe::AudioStream *outputStream = getOutputStream();
601 oboe::AudioStream *inputStream = getInputStream();
602 if (outputStream) {
603 mSinkFloat = std::make_unique<SinkFloat>(mChannelCount);
604 sineOscillator = std::make_unique<SineOscillator>();
605 monoToMulti = std::make_unique<MonoToMultiConverter>(mChannelCount);
606
607 sineOscillator->setSampleRate(outputStream->getSampleRate());
608 sineOscillator->frequency.setValue(440.0);
609 sineOscillator->amplitude.setValue(AMPLITUDE_SINE);
610 sineOscillator->output.connect(&(monoToMulti->input));
611
612 monoToMulti->output.connect(&(mSinkFloat->input));
613 mSinkFloat->pullReset();
614 audioStreamGateway.setAudioSink(mSinkFloat);
615 } else if (inputStream) {
616 audioStreamGateway.setAudioSink(nullptr);
617 }
618 oboeCallbackProxy.setCallback(&audioStreamGateway);
619 }
620
621