| // Copyright 2016 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "third_party/blink/renderer/modules/webaudio/iir_filter_node.h" |
| |
| #include <memory> |
| |
| #include "base/metrics/histogram_functions.h" |
| #include "third_party/blink/renderer/bindings/modules/v8/v8_iir_filter_options.h" |
| #include "third_party/blink/renderer/core/inspector/console_message.h" |
| #include "third_party/blink/renderer/modules/webaudio/audio_node_output.h" |
| #include "third_party/blink/renderer/modules/webaudio/base_audio_context.h" |
| #include "third_party/blink/renderer/platform/bindings/exception_messages.h" |
| #include "third_party/blink/renderer/platform/bindings/exception_state.h" |
| #include "third_party/blink/renderer/platform/heap/heap.h" |
| #include "third_party/blink/renderer/platform/scheduler/public/post_cross_thread_task.h" |
| #include "third_party/blink/renderer/platform/wtf/cross_thread_functional.h" |
| |
| namespace blink { |
| |
| IIRFilterHandler::IIRFilterHandler(AudioNode& node, |
| float sample_rate, |
| const Vector<double>& feedforward_coef, |
| const Vector<double>& feedback_coef, |
| bool is_filter_stable) |
| : AudioBasicProcessorHandler( |
| kNodeTypeIIRFilter, |
| node, |
| sample_rate, |
| std::make_unique<IIRProcessor>(sample_rate, |
| 1, |
| feedforward_coef, |
| feedback_coef, |
| is_filter_stable)) { |
| DCHECK(Context()); |
| DCHECK(Context()->GetExecutionContext()); |
| |
| task_runner_ = Context()->GetExecutionContext()->GetTaskRunner( |
| TaskType::kMediaElementEvent); |
| } |
| |
| scoped_refptr<IIRFilterHandler> IIRFilterHandler::Create( |
| AudioNode& node, |
| float sample_rate, |
| const Vector<double>& feedforward_coef, |
| const Vector<double>& feedback_coef, |
| bool is_filter_stable) { |
| return base::AdoptRef(new IIRFilterHandler( |
| node, sample_rate, feedforward_coef, feedback_coef, is_filter_stable)); |
| } |
| |
| // Determine if filter is stable based on the feedback coefficients. |
| // We compute the reflection coefficients for the filter. If, at any |
| // point, the magnitude of the reflection coefficient is greater than |
| // or equal to 1, the filter is declared unstable. |
| // |
| // Let A(z) be the feedback polynomial given by |
| // A[n](z) = 1 + a[1]/z + a[2]/z^2 + ... + a[n]/z^n |
| // |
| // The first reflection coefficient k[n] = a[n]. Then, recursively compute |
| // |
| // A[n-1](z) = (A[n](z) - k[n]*A[n](1/z)/z^n)/(1-k[n]^2); |
| // |
| // stopping at A[1](z). If at any point |k[n]| >= 1, the filter is |
| // unstable. |
| static bool IsFilterStable(const Vector<double>& feedback_coef) { |
| // Make a copy of the feedback coefficients |
| Vector<double> coef(feedback_coef); |
| int order = coef.size() - 1; |
| |
| // If necessary, normalize filter coefficients so that constant term is 1. |
| if (coef[0] != 1) { |
| for (int m = 1; m <= order; ++m) |
| coef[m] /= coef[0]; |
| coef[0] = 1; |
| } |
| |
| // Begin recursion, using a work array to hold intermediate results. |
| Vector<double> work(order + 1); |
| for (int n = order; n >= 1; --n) { |
| double k = coef[n]; |
| |
| if (std::fabs(k) >= 1) |
| return false; |
| |
| // Note that A[n](1/z)/z^n is basically the coefficients of A[n] |
| // in reverse order. |
| double factor = 1 - k * k; |
| for (int m = 0; m <= n; ++m) |
| work[m] = (coef[m] - k * coef[n - m]) / factor; |
| coef.swap(work); |
| } |
| |
| return true; |
| } |
| |
| void IIRFilterHandler::Process(uint32_t frames_to_process) { |
| AudioBasicProcessorHandler::Process(frames_to_process); |
| |
| if (!did_warn_bad_filter_state_) { |
| // Inform the user once if the output has a non-finite value. This is a |
| // proxy for the filter state containing non-finite values since the output |
| // is also saved as part of the state of the filter. |
| if (HasNonFiniteOutput()) { |
| did_warn_bad_filter_state_ = true; |
| |
| PostCrossThreadTask( |
| *task_runner_, FROM_HERE, |
| CrossThreadBindOnce(&IIRFilterHandler::NotifyBadState, AsWeakPtr())); |
| } |
| } |
| } |
| |
| void IIRFilterHandler::NotifyBadState() const { |
| DCHECK(IsMainThread()); |
| if (!Context() || !Context()->GetExecutionContext()) |
| return; |
| |
| Context()->GetExecutionContext()->AddConsoleMessage( |
| MakeGarbageCollected<ConsoleMessage>( |
| mojom::ConsoleMessageSource::kJavaScript, |
| mojom::ConsoleMessageLevel::kWarning, |
| NodeTypeName() + ": state is bad, probably due to unstable filter.")); |
| } |
| |
| IIRFilterNode::IIRFilterNode(BaseAudioContext& context, |
| const Vector<double>& feedforward_coef, |
| const Vector<double>& feedback_coef, |
| bool is_filter_stable) |
| : AudioNode(context) { |
| SetHandler(IIRFilterHandler::Create(*this, context.sampleRate(), |
| feedforward_coef, feedback_coef, |
| is_filter_stable)); |
| |
| // Histogram of the IIRFilter order. createIIRFilter ensures that the length |
| // of |feedbackCoef| is in the range [1, IIRFilter::kMaxOrder + 1]. The order |
| // is one less than the length of this vector. |
| base::UmaHistogramSparse("WebAudio.IIRFilterNode.Order", |
| feedback_coef.size() - 1); |
| } |
| |
| IIRFilterNode* IIRFilterNode::Create(BaseAudioContext& context, |
| const Vector<double>& feedforward_coef, |
| const Vector<double>& feedback_coef, |
| ExceptionState& exception_state) { |
| DCHECK(IsMainThread()); |
| |
| // TODO(crbug.com/1055983): Remove this when the execution context validity |
| // check is not required in the AudioNode factory methods. |
| if (!context.CheckExecutionContextAndThrowIfNecessary(exception_state)) |
| return nullptr; |
| |
| if (feedback_coef.size() == 0 || |
| (feedback_coef.size() > IIRFilter::kMaxOrder + 1)) { |
| exception_state.ThrowDOMException( |
| DOMExceptionCode::kNotSupportedError, |
| ExceptionMessages::IndexOutsideRange<size_t>( |
| "number of feedback coefficients", feedback_coef.size(), 1, |
| ExceptionMessages::kInclusiveBound, IIRFilter::kMaxOrder + 1, |
| ExceptionMessages::kInclusiveBound)); |
| return nullptr; |
| } |
| |
| if (feedforward_coef.size() == 0 || |
| (feedforward_coef.size() > IIRFilter::kMaxOrder + 1)) { |
| exception_state.ThrowDOMException( |
| DOMExceptionCode::kNotSupportedError, |
| ExceptionMessages::IndexOutsideRange<size_t>( |
| "number of feedforward coefficients", feedforward_coef.size(), 1, |
| ExceptionMessages::kInclusiveBound, IIRFilter::kMaxOrder + 1, |
| ExceptionMessages::kInclusiveBound)); |
| return nullptr; |
| } |
| |
| if (feedback_coef[0] == 0) { |
| exception_state.ThrowDOMException( |
| DOMExceptionCode::kInvalidStateError, |
| "First feedback coefficient cannot be zero."); |
| return nullptr; |
| } |
| |
| bool has_non_zero_coef = false; |
| |
| for (wtf_size_t k = 0; k < feedforward_coef.size(); ++k) { |
| if (feedforward_coef[k] != 0) { |
| has_non_zero_coef = true; |
| break; |
| } |
| } |
| |
| if (!has_non_zero_coef) { |
| exception_state.ThrowDOMException( |
| DOMExceptionCode::kInvalidStateError, |
| "At least one feedforward coefficient must be non-zero."); |
| return nullptr; |
| } |
| |
| bool is_filter_stable = IsFilterStable(feedback_coef); |
| if (!is_filter_stable) { |
| StringBuilder message; |
| message.Append("Unstable IIRFilter with feedback coefficients: ["); |
| message.AppendNumber(feedback_coef[0]); |
| for (wtf_size_t k = 1; k < feedback_coef.size(); ++k) { |
| message.Append(", "); |
| message.AppendNumber(feedback_coef[k]); |
| } |
| message.Append(']'); |
| |
| context.GetExecutionContext()->AddConsoleMessage( |
| MakeGarbageCollected<ConsoleMessage>( |
| mojom::ConsoleMessageSource::kJavaScript, |
| mojom::ConsoleMessageLevel::kWarning, message.ToString())); |
| } |
| |
| return MakeGarbageCollected<IIRFilterNode>(context, feedforward_coef, |
| feedback_coef, is_filter_stable); |
| } |
| |
| IIRFilterNode* IIRFilterNode::Create(BaseAudioContext* context, |
| const IIRFilterOptions* options, |
| ExceptionState& exception_state) { |
| IIRFilterNode* node = Create(*context, options->feedforward(), |
| options->feedback(), exception_state); |
| |
| if (!node) |
| return nullptr; |
| |
| node->HandleChannelOptions(options, exception_state); |
| |
| return node; |
| } |
| |
| void IIRFilterNode::Trace(Visitor* visitor) const { |
| AudioNode::Trace(visitor); |
| } |
| |
| IIRProcessor* IIRFilterNode::GetIIRFilterProcessor() const { |
| return static_cast<IIRProcessor*>( |
| static_cast<IIRFilterHandler&>(Handler()).Processor()); |
| } |
| |
| void IIRFilterNode::getFrequencyResponse( |
| NotShared<const DOMFloat32Array> frequency_hz, |
| NotShared<DOMFloat32Array> mag_response, |
| NotShared<DOMFloat32Array> phase_response, |
| ExceptionState& exception_state) { |
| size_t frequency_hz_length = frequency_hz->length(); |
| |
| // All the arrays must have the same length. Just verify that all |
| // the arrays have the same length as the |frequency_hz| array. |
| if (mag_response->length() != frequency_hz_length) { |
| exception_state.ThrowDOMException( |
| DOMExceptionCode::kInvalidAccessError, |
| ExceptionMessages::IndexOutsideRange( |
| "magResponse length", mag_response->length(), frequency_hz_length, |
| ExceptionMessages::kInclusiveBound, frequency_hz_length, |
| ExceptionMessages::kInclusiveBound)); |
| return; |
| } |
| |
| if (phase_response->length() != frequency_hz_length) { |
| exception_state.ThrowDOMException( |
| DOMExceptionCode::kInvalidAccessError, |
| ExceptionMessages::IndexOutsideRange( |
| "phaseResponse length", phase_response->length(), |
| frequency_hz_length, ExceptionMessages::kInclusiveBound, |
| frequency_hz_length, ExceptionMessages::kInclusiveBound)); |
| return; |
| } |
| |
| int frequency_hz_length_as_int; |
| if (!base::CheckedNumeric<int>(frequency_hz_length) |
| .AssignIfValid(&frequency_hz_length_as_int)) { |
| exception_state.ThrowRangeError( |
| "frequencyHz length exceeds the maximum supported length"); |
| return; |
| } |
| |
| // Nothing to do if the length is 0. |
| if (frequency_hz_length_as_int > 0) { |
| GetIIRFilterProcessor()->GetFrequencyResponse( |
| frequency_hz_length_as_int, frequency_hz->Data(), mag_response->Data(), |
| phase_response->Data()); |
| } |
| } |
| |
| void IIRFilterNode::ReportDidCreate() { |
| GraphTracer().DidCreateAudioNode(this); |
| } |
| |
| void IIRFilterNode::ReportWillBeDestroyed() { |
| GraphTracer().WillDestroyAudioNode(this); |
| } |
| |
| } // namespace blink |