| @@ -13,7 +13,166 @@ namespace core { | |||
| template <int NUM_AUDIO_INPUTS, int NUM_AUDIO_OUTPUTS> | |||
| struct AudioInterface : Module, audio::Port { | |||
| struct AudioInterfacePort : audio::Port { | |||
| Module* module; | |||
| dsp::DoubleRingBuffer<dsp::Frame<NUM_AUDIO_INPUTS>, 32768> engineInputBuffer; | |||
| dsp::DoubleRingBuffer<dsp::Frame<NUM_AUDIO_OUTPUTS>, 32768> engineOutputBuffer; | |||
| dsp::SampleRateConverter<NUM_AUDIO_INPUTS> inputSrc; | |||
| dsp::SampleRateConverter<NUM_AUDIO_OUTPUTS> outputSrc; | |||
| // Port variable caches | |||
| int deviceNumInputs = 0; | |||
| int deviceNumOutputs = 0; | |||
| float deviceSampleRate = 0.f; | |||
| int requestedEngineFrames = 0; | |||
| AudioInterfacePort(Module* module) { | |||
| this->module = module; | |||
| maxOutputs = NUM_AUDIO_INPUTS; | |||
| maxInputs = NUM_AUDIO_OUTPUTS; | |||
| inputSrc.setQuality(6); | |||
| outputSrc.setQuality(6); | |||
| } | |||
| void setPrimary() { | |||
| APP->engine->setPrimaryModule(module); | |||
| } | |||
| bool isPrimary() { | |||
| return APP->engine->getPrimaryModule() == module; | |||
| } | |||
| void processInput(const float* input, int inputStride, int frames) override { | |||
| // DEBUG("%p: new device block ____________________________", this); | |||
| // Claim primary module if there is none | |||
| if (!APP->engine->getPrimaryModule()) { | |||
| setPrimary(); | |||
| } | |||
| bool isPrimaryCached = isPrimary(); | |||
| // Set sample rate of engine if engine sample rate is "auto". | |||
| if (isPrimaryCached) { | |||
| APP->engine->setSuggestedSampleRate(deviceSampleRate); | |||
| } | |||
| float engineSampleRate = APP->engine->getSampleRate(); | |||
| float sampleRateRatio = engineSampleRate / deviceSampleRate; | |||
| // DEBUG("%p: %d block, engineOutputBuffer still has %d", this, frames, (int) engineOutputBuffer.size()); | |||
| // Consider engine buffers "too full" if they contain a bit more than the audio device's number of frames, converted to engine sample rate. | |||
| int maxEngineFrames = (int) std::ceil(frames * sampleRateRatio * 2.0) - 1; | |||
| // If the engine output buffer is too full, clear it to keep latency low. No need to clear if primary because it's always cleared below. | |||
| if (!isPrimaryCached && (int) engineOutputBuffer.size() > maxEngineFrames) { | |||
| engineOutputBuffer.clear(); | |||
| // DEBUG("%p: clearing engine output", this); | |||
| } | |||
| if (deviceNumInputs > 0) { | |||
| // Always clear engine output if primary | |||
| if (isPrimaryCached) { | |||
| engineOutputBuffer.clear(); | |||
| } | |||
| // Set up sample rate converter | |||
| outputSrc.setRates(deviceSampleRate, engineSampleRate); | |||
| outputSrc.setChannels(deviceNumInputs); | |||
| // Convert audio input -> engine output | |||
| dsp::Frame<NUM_AUDIO_OUTPUTS> audioInputBuffer[frames]; | |||
| std::memset(audioInputBuffer, 0, sizeof(audioInputBuffer)); | |||
| for (int i = 0; i < frames; i++) { | |||
| for (int j = 0; j < deviceNumInputs; j++) { | |||
| float v = input[i * inputStride + j]; | |||
| audioInputBuffer[i].samples[j] = v; | |||
| } | |||
| } | |||
| int audioInputFrames = frames; | |||
| int outputFrames = engineOutputBuffer.capacity(); | |||
| outputSrc.process(audioInputBuffer, &audioInputFrames, engineOutputBuffer.endData(), &outputFrames); | |||
| engineOutputBuffer.endIncr(outputFrames); | |||
| // Request exactly as many frames as we have in the engine output buffer. | |||
| requestedEngineFrames = engineOutputBuffer.size(); | |||
| } | |||
| else { | |||
| // Upper bound on number of frames so that `audioOutputFrames >= frames` when processOutput() is called. | |||
| requestedEngineFrames = std::max((int) std::ceil(frames * sampleRateRatio) - (int) engineInputBuffer.size(), 0); | |||
| } | |||
| } | |||
| void processBuffer(const float* input, int inputStride, float* output, int outputStride, int frames) override { | |||
| // Step engine | |||
| if (isPrimary() && requestedEngineFrames > 0) { | |||
| // DEBUG("%p: %d block, stepping %d", this, frames, requestedEngineFrames); | |||
| APP->engine->stepBlock(requestedEngineFrames); | |||
| } | |||
| } | |||
| void processOutput(float* output, int outputStride, int frames) override { | |||
| // bool isPrimaryCached = isPrimary(); | |||
| float engineSampleRate = APP->engine->getSampleRate(); | |||
| float sampleRateRatio = engineSampleRate / deviceSampleRate; | |||
| if (deviceNumOutputs > 0) { | |||
| // Set up sample rate converter | |||
| inputSrc.setRates(engineSampleRate, deviceSampleRate); | |||
| inputSrc.setChannels(deviceNumOutputs); | |||
| // Convert engine input -> audio output | |||
| dsp::Frame<NUM_AUDIO_OUTPUTS> audioOutputBuffer[frames]; | |||
| int inputFrames = engineInputBuffer.size(); | |||
| int audioOutputFrames = frames; | |||
| inputSrc.process(engineInputBuffer.startData(), &inputFrames, audioOutputBuffer, &audioOutputFrames); | |||
| engineInputBuffer.startIncr(inputFrames); | |||
| // Copy the audio output buffer | |||
| for (int i = 0; i < audioOutputFrames; i++) { | |||
| for (int j = 0; j < deviceNumOutputs; j++) { | |||
| float v = audioOutputBuffer[i].samples[j]; | |||
| v = clamp(v, -1.f, 1.f); | |||
| output[i * outputStride + j] = v; | |||
| } | |||
| } | |||
| // Fill the rest of the audio output buffer with zeros | |||
| for (int i = audioOutputFrames; i < frames; i++) { | |||
| for (int j = 0; j < deviceNumOutputs; j++) { | |||
| output[i * outputStride + j] = 0.f; | |||
| } | |||
| } | |||
| } | |||
| // DEBUG("%p: %d block, engineInputBuffer left %d", this, frames, (int) engineInputBuffer.size()); | |||
| // If the engine input buffer is too full, clear it to keep latency low. | |||
| int maxEngineFrames = (int) std::ceil(frames * sampleRateRatio * 2.0) - 1; | |||
| if ((int) engineInputBuffer.size() > maxEngineFrames) { | |||
| engineInputBuffer.clear(); | |||
| // DEBUG("%p: clearing engine input", this); | |||
| } | |||
| // DEBUG("%p %s:\tframes %d requestedEngineFrames %d\toutputBuffer %d engineInputBuffer %d\t", this, isPrimaryCached ? "primary" : "secondary", frames, requestedEngineFrames, engineOutputBuffer.size(), engineInputBuffer.size()); | |||
| } | |||
| void onStartStream() override { | |||
| deviceNumInputs = std::min(getNumInputs(), NUM_AUDIO_OUTPUTS); | |||
| deviceNumOutputs = std::min(getNumOutputs(), NUM_AUDIO_INPUTS); | |||
| deviceSampleRate = getSampleRate(); | |||
| engineInputBuffer.clear(); | |||
| engineOutputBuffer.clear(); | |||
| // DEBUG("onStartStream %d %d %f", deviceNumInputs, deviceNumOutputs, deviceSampleRate); | |||
| } | |||
| void onStopStream() override { | |||
| deviceNumInputs = 0; | |||
| deviceNumOutputs = 0; | |||
| deviceSampleRate = 0.f; | |||
| engineInputBuffer.clear(); | |||
| engineOutputBuffer.clear(); | |||
| // DEBUG("onStopStream"); | |||
| } | |||
| }; | |||
| template <int NUM_AUDIO_INPUTS, int NUM_AUDIO_OUTPUTS> | |||
| struct AudioInterface : Module { | |||
| static constexpr int NUM_INPUT_LIGHTS = (NUM_AUDIO_INPUTS > 2) ? (NUM_AUDIO_INPUTS / 2) : 0; | |||
| static constexpr int NUM_OUTPUT_LIGHTS = (NUM_AUDIO_OUTPUTS > 2) ? (NUM_AUDIO_OUTPUTS / 2) : 0; | |||
| @@ -36,28 +195,18 @@ struct AudioInterface : Module, audio::Port { | |||
| NUM_LIGHTS | |||
| }; | |||
| AudioInterfacePort<NUM_AUDIO_INPUTS, NUM_AUDIO_OUTPUTS> port; | |||
| dsp::RCFilter dcFilters[NUM_AUDIO_INPUTS]; | |||
| bool dcFilterEnabled = false; | |||
| dsp::DoubleRingBuffer<dsp::Frame<NUM_AUDIO_INPUTS>, 32768> engineInputBuffer; | |||
| dsp::DoubleRingBuffer<dsp::Frame<NUM_AUDIO_OUTPUTS>, 32768> engineOutputBuffer; | |||
| dsp::SampleRateConverter<NUM_AUDIO_INPUTS> inputSrc; | |||
| dsp::SampleRateConverter<NUM_AUDIO_OUTPUTS> outputSrc; | |||
| dsp::ClockDivider lightDivider; | |||
| // For each pair of inputs/outputs | |||
| float inputClipTimers[(NUM_AUDIO_INPUTS > 0) ? NUM_INPUT_LIGHTS : 0] = {}; | |||
| float outputClipTimers[(NUM_AUDIO_INPUTS > 0) ? NUM_OUTPUT_LIGHTS : 0] = {}; | |||
| dsp::VuMeter2 vuMeter[(NUM_AUDIO_INPUTS == 2) ? 2 : 0]; | |||
| // Port variable caches | |||
| int deviceNumInputs = 0; | |||
| int deviceNumOutputs = 0; | |||
| float deviceSampleRate = 0.f; | |||
| int requestedEngineFrames = 0; | |||
| AudioInterface() { | |||
| AudioInterface() : port(this) { | |||
| config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); | |||
| if (NUM_AUDIO_INPUTS == 2) | |||
| configParam(GAIN_PARAM, 0.f, 2.f, 1.f, "Level", " dB", -10, 40); | |||
| @@ -71,26 +220,22 @@ struct AudioInterface : Module, audio::Port { | |||
| configLight(OUTPUT_LIGHTS + 2 * i, string::f("Device input %d/%d status", 2 * i + 1, 2 * i + 2)); | |||
| lightDivider.setDivision(512); | |||
| maxOutputs = NUM_AUDIO_INPUTS; | |||
| maxInputs = NUM_AUDIO_OUTPUTS; | |||
| inputSrc.setQuality(6); | |||
| outputSrc.setQuality(6); | |||
| float sampleTime = APP->engine->getSampleTime(); | |||
| for (int i = 0; i < NUM_AUDIO_INPUTS; i++) { | |||
| dcFilters[i].setCutoffFreq(10.f * sampleTime); | |||
| } | |||
| reset(); | |||
| onReset(); | |||
| } | |||
| ~AudioInterface() { | |||
| // Close stream here before destructing AudioInterfacePort, so processBuffer() etc are not called on another thread while destructing. | |||
| setDriverId(-1); | |||
| port.setDriverId(-1); | |||
| } | |||
| void onReset() override { | |||
| setDriverId(-1); | |||
| port.setDriverId(-1); | |||
| if (NUM_AUDIO_INPUTS == 2) | |||
| dcFilterEnabled = true; | |||
| @@ -99,8 +244,8 @@ struct AudioInterface : Module, audio::Port { | |||
| } | |||
| void onSampleRateChange(const SampleRateChangeEvent& e) override { | |||
| engineInputBuffer.clear(); | |||
| engineOutputBuffer.clear(); | |||
| port.engineInputBuffer.clear(); | |||
| port.engineOutputBuffer.clear(); | |||
| for (int i = 0; i < NUM_AUDIO_INPUTS; i++) { | |||
| dcFilters[i].setCutoffFreq(10.f * e.sampleTime); | |||
| @@ -111,9 +256,9 @@ struct AudioInterface : Module, audio::Port { | |||
| const float clipTime = 0.25f; | |||
| // Push inputs to buffer | |||
| if (deviceNumOutputs > 0) { | |||
| if (port.deviceNumOutputs > 0) { | |||
| dsp::Frame<NUM_AUDIO_INPUTS> inputFrame = {}; | |||
| for (int i = 0; i < deviceNumOutputs; i++) { | |||
| for (int i = 0; i < port.deviceNumOutputs; i++) { | |||
| // Get input | |||
| float v = 0.f; | |||
| if (inputs[AUDIO_INPUTS + i].isConnected()) | |||
| @@ -144,8 +289,8 @@ struct AudioInterface : Module, audio::Port { | |||
| } | |||
| } | |||
| if (!engineInputBuffer.full()) { | |||
| engineInputBuffer.push(inputFrame); | |||
| if (!port.engineInputBuffer.full()) { | |||
| port.engineInputBuffer.push(inputFrame); | |||
| } | |||
| // Audio-2: VU meter process | |||
| @@ -165,8 +310,8 @@ struct AudioInterface : Module, audio::Port { | |||
| } | |||
| // Pull outputs from buffer | |||
| if (!engineOutputBuffer.empty()) { | |||
| dsp::Frame<NUM_AUDIO_OUTPUTS> outputFrame = engineOutputBuffer.shift(); | |||
| if (!port.engineOutputBuffer.empty()) { | |||
| dsp::Frame<NUM_AUDIO_OUTPUTS> outputFrame = port.engineOutputBuffer.shift(); | |||
| for (int i = 0; i < NUM_AUDIO_OUTPUTS; i++) { | |||
| float v = outputFrame.samples[i]; | |||
| outputs[AUDIO_OUTPUTS + i].setVoltage(10.f * v); | |||
| @@ -203,7 +348,7 @@ struct AudioInterface : Module, audio::Port { | |||
| else { | |||
| // Turn on light if at least one port is enabled in the nearby pair. | |||
| for (int i = 0; i < NUM_AUDIO_INPUTS / 2; i++) { | |||
| bool active = deviceNumOutputs >= 2 * i + 1; | |||
| bool active = port.deviceNumOutputs >= 2 * i + 1; | |||
| bool clip = inputClipTimers[i] > 0.f; | |||
| if (clip) | |||
| inputClipTimers[i] -= lightTime; | |||
| @@ -211,7 +356,7 @@ struct AudioInterface : Module, audio::Port { | |||
| lights[INPUT_LIGHTS + i * 2 + 1].setBrightness(active && clip); | |||
| } | |||
| for (int i = 0; i < NUM_AUDIO_OUTPUTS / 2; i++) { | |||
| bool active = deviceNumInputs >= 2 * i + 1; | |||
| bool active = port.deviceNumInputs >= 2 * i + 1; | |||
| bool clip = outputClipTimers[i] > 0.f; | |||
| if (clip) | |||
| outputClipTimers[i] -= lightTime; | |||
| @@ -224,7 +369,7 @@ struct AudioInterface : Module, audio::Port { | |||
| json_t* dataToJson() override { | |||
| json_t* rootJ = json_object(); | |||
| json_object_set_new(rootJ, "audio", audio::Port::toJson()); | |||
| json_object_set_new(rootJ, "audio", port.toJson()); | |||
| if (isPrimary()) | |||
| json_object_set_new(rootJ, "primary", json_boolean(true)); | |||
| @@ -237,7 +382,7 @@ struct AudioInterface : Module, audio::Port { | |||
| void dataFromJson(json_t* rootJ) override { | |||
| json_t* audioJ = json_object_get(rootJ, "audio"); | |||
| if (audioJ) | |||
| audio::Port::fromJson(audioJ); | |||
| port.fromJson(audioJ); | |||
| // For not, don't deserialize primary module state. | |||
| // json_t* primaryJ = json_object_get(rootJ, "primary"); | |||
| @@ -258,133 +403,6 @@ struct AudioInterface : Module, audio::Port { | |||
| bool isPrimary() { | |||
| return APP->engine->getPrimaryModule() == this; | |||
| } | |||
| // audio::Port | |||
| void processInput(const float* input, int inputStride, int frames) override { | |||
| // DEBUG("%p: new device block ____________________________", this); | |||
| // Claim primary module if there is none | |||
| if (!APP->engine->getPrimaryModule()) { | |||
| setPrimary(); | |||
| } | |||
| bool isPrimaryCached = isPrimary(); | |||
| // Set sample rate of engine if engine sample rate is "auto". | |||
| if (isPrimaryCached) { | |||
| APP->engine->setSuggestedSampleRate(deviceSampleRate); | |||
| } | |||
| float engineSampleRate = APP->engine->getSampleRate(); | |||
| float sampleRateRatio = engineSampleRate / deviceSampleRate; | |||
| // DEBUG("%p: %d block, engineOutputBuffer still has %d", this, frames, (int) engineOutputBuffer.size()); | |||
| // Consider engine buffers "too full" if they contain a bit more than the audio device's number of frames, converted to engine sample rate. | |||
| int maxEngineFrames = (int) std::ceil(frames * sampleRateRatio * 2.0) - 1; | |||
| // If the engine output buffer is too full, clear it to keep latency low. No need to clear if primary because it's always cleared below. | |||
| if (!isPrimaryCached && (int) engineOutputBuffer.size() > maxEngineFrames) { | |||
| engineOutputBuffer.clear(); | |||
| // DEBUG("%p: clearing engine output", this); | |||
| } | |||
| if (deviceNumInputs > 0) { | |||
| // Always clear engine output if primary | |||
| if (isPrimaryCached) { | |||
| engineOutputBuffer.clear(); | |||
| } | |||
| // Set up sample rate converter | |||
| outputSrc.setRates(deviceSampleRate, engineSampleRate); | |||
| outputSrc.setChannels(deviceNumInputs); | |||
| // Convert audio input -> engine output | |||
| dsp::Frame<NUM_AUDIO_OUTPUTS> audioInputBuffer[frames]; | |||
| std::memset(audioInputBuffer, 0, sizeof(audioInputBuffer)); | |||
| for (int i = 0; i < frames; i++) { | |||
| for (int j = 0; j < deviceNumInputs; j++) { | |||
| float v = input[i * inputStride + j]; | |||
| audioInputBuffer[i].samples[j] = v; | |||
| } | |||
| } | |||
| int audioInputFrames = frames; | |||
| int outputFrames = engineOutputBuffer.capacity(); | |||
| outputSrc.process(audioInputBuffer, &audioInputFrames, engineOutputBuffer.endData(), &outputFrames); | |||
| engineOutputBuffer.endIncr(outputFrames); | |||
| // Request exactly as many frames as we have in the engine output buffer. | |||
| requestedEngineFrames = engineOutputBuffer.size(); | |||
| } | |||
| else { | |||
| // Upper bound on number of frames so that `audioOutputFrames >= frames` when processOutput() is called. | |||
| requestedEngineFrames = std::max((int) std::ceil(frames * sampleRateRatio) - (int) engineInputBuffer.size(), 0); | |||
| } | |||
| } | |||
| void processBuffer(const float* input, int inputStride, float* output, int outputStride, int frames) override { | |||
| // Step engine | |||
| if (isPrimary() && requestedEngineFrames > 0) { | |||
| // DEBUG("%p: %d block, stepping %d", this, frames, requestedEngineFrames); | |||
| APP->engine->stepBlock(requestedEngineFrames); | |||
| } | |||
| } | |||
| void processOutput(float* output, int outputStride, int frames) override { | |||
| // bool isPrimaryCached = isPrimary(); | |||
| float engineSampleRate = APP->engine->getSampleRate(); | |||
| float sampleRateRatio = engineSampleRate / deviceSampleRate; | |||
| if (deviceNumOutputs > 0) { | |||
| // Set up sample rate converter | |||
| inputSrc.setRates(engineSampleRate, deviceSampleRate); | |||
| inputSrc.setChannels(deviceNumOutputs); | |||
| // Convert engine input -> audio output | |||
| dsp::Frame<NUM_AUDIO_OUTPUTS> audioOutputBuffer[frames]; | |||
| int inputFrames = engineInputBuffer.size(); | |||
| int audioOutputFrames = frames; | |||
| inputSrc.process(engineInputBuffer.startData(), &inputFrames, audioOutputBuffer, &audioOutputFrames); | |||
| engineInputBuffer.startIncr(inputFrames); | |||
| // Copy the audio output buffer | |||
| for (int i = 0; i < audioOutputFrames; i++) { | |||
| for (int j = 0; j < deviceNumOutputs; j++) { | |||
| float v = audioOutputBuffer[i].samples[j]; | |||
| v = clamp(v, -1.f, 1.f); | |||
| output[i * outputStride + j] = v; | |||
| } | |||
| } | |||
| // Fill the rest of the audio output buffer with zeros | |||
| for (int i = audioOutputFrames; i < frames; i++) { | |||
| for (int j = 0; j < deviceNumOutputs; j++) { | |||
| output[i * outputStride + j] = 0.f; | |||
| } | |||
| } | |||
| } | |||
| // DEBUG("%p: %d block, engineInputBuffer left %d", this, frames, (int) engineInputBuffer.size()); | |||
| // If the engine input buffer is too full, clear it to keep latency low. | |||
| int maxEngineFrames = (int) std::ceil(frames * sampleRateRatio * 2.0) - 1; | |||
| if ((int) engineInputBuffer.size() > maxEngineFrames) { | |||
| engineInputBuffer.clear(); | |||
| // DEBUG("%p: clearing engine input", this); | |||
| } | |||
| // DEBUG("%p %s:\tframes %d requestedEngineFrames %d\toutputBuffer %d engineInputBuffer %d\t", this, isPrimaryCached ? "primary" : "secondary", frames, requestedEngineFrames, engineOutputBuffer.size(), engineInputBuffer.size()); | |||
| } | |||
| void onStartStream() override { | |||
| deviceNumInputs = std::min(getNumInputs(), NUM_AUDIO_OUTPUTS); | |||
| deviceNumOutputs = std::min(getNumOutputs(), NUM_AUDIO_INPUTS); | |||
| deviceSampleRate = getSampleRate(); | |||
| engineInputBuffer.clear(); | |||
| engineOutputBuffer.clear(); | |||
| // DEBUG("onStartStream %d %d %f", deviceNumInputs, deviceNumOutputs, deviceSampleRate); | |||
| } | |||
| void onStopStream() override { | |||
| deviceNumInputs = 0; | |||
| deviceNumOutputs = 0; | |||
| deviceSampleRate = 0.f; | |||
| engineInputBuffer.clear(); | |||
| engineOutputBuffer.clear(); | |||
| // DEBUG("onStopStream"); | |||
| } | |||
| }; | |||
| @@ -432,7 +450,7 @@ struct AudioInterfaceWidget : ModuleWidget { | |||
| AudioWidget* audioWidget = createWidget<AudioWidget>(mm2px(Vec(3.2122073, 14.837339))); | |||
| audioWidget->box.size = mm2px(Vec(44, 28)); | |||
| audioWidget->setAudioPort(module); | |||
| audioWidget->setAudioPort(module ? &module->port : NULL); | |||
| addChild(audioWidget); | |||
| } | |||
| else if (NUM_AUDIO_INPUTS == 16 && NUM_AUDIO_OUTPUTS == 16) { | |||
| @@ -496,7 +514,7 @@ struct AudioInterfaceWidget : ModuleWidget { | |||
| AudioWidget* audioWidget = createWidget<AudioWidget>(mm2px(Vec(2.57, 14.839))); | |||
| audioWidget->box.size = mm2px(Vec(91.382, 28.0)); | |||
| audioWidget->setAudioPort(module); | |||
| audioWidget->setAudioPort(module ? &module->port : NULL); | |||
| addChild(audioWidget); | |||
| } | |||
| else if (NUM_AUDIO_INPUTS == 2 && NUM_AUDIO_OUTPUTS == 2) { | |||
| @@ -530,7 +548,7 @@ struct AudioInterfaceWidget : ModuleWidget { | |||
| AudioDeviceWidget* audioWidget = createWidget<AudioDeviceWidget>(mm2px(Vec(2.135, 14.259))); | |||
| audioWidget->box.size = mm2px(Vec(21.128, 6.725)); | |||
| audioWidget->setAudioPort(module); | |||
| audioWidget->setAudioPort(module ? &module->port : NULL); | |||
| // Adjust deviceChoice position | |||
| audioWidget->deviceChoice->textOffset = Vec(6, 14); | |||
| addChild(audioWidget); | |||