| /* |
| * Copyright (C) 2010, Google Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND |
| * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
| * ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE |
| * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
| * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR |
| * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
| * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT |
| * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY |
| * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH |
| * DAMAGE. |
| */ |
| |
| #include "modules/webaudio/RealtimeAnalyser.h" |
| #include "platform/audio/AudioBus.h" |
| #include "platform/audio/AudioUtilities.h" |
| #include "platform/audio/VectorMath.h" |
| #include "wtf/MathExtras.h" |
| #include "wtf/PtrUtil.h" |
| #include <algorithm> |
| #include <complex> |
| #include <limits.h> |
| |
| namespace blink { |
| |
| const double RealtimeAnalyser::DefaultSmoothingTimeConstant = 0.8; |
| const double RealtimeAnalyser::DefaultMinDecibels = -100; |
| const double RealtimeAnalyser::DefaultMaxDecibels = -30; |
| |
| const unsigned RealtimeAnalyser::DefaultFFTSize = 2048; |
| // All FFT implementations are expected to handle power-of-two sizes |
| // MinFFTSize <= size <= MaxFFTSize. |
| const unsigned RealtimeAnalyser::MinFFTSize = 32; |
| const unsigned RealtimeAnalyser::MaxFFTSize = 32768; |
| const unsigned RealtimeAnalyser::InputBufferSize = |
| RealtimeAnalyser::MaxFFTSize * 2; |
| |
| RealtimeAnalyser::RealtimeAnalyser() |
| : m_inputBuffer(InputBufferSize), |
| m_writeIndex(0), |
| m_downMixBus(AudioBus::create(1, AudioUtilities::kRenderQuantumFrames)), |
| m_fftSize(DefaultFFTSize), |
| m_magnitudeBuffer(DefaultFFTSize / 2), |
| m_smoothingTimeConstant(DefaultSmoothingTimeConstant), |
| m_minDecibels(DefaultMinDecibels), |
| m_maxDecibels(DefaultMaxDecibels), |
| m_lastAnalysisTime(-1) { |
| m_analysisFrame = wrapUnique(new FFTFrame(DefaultFFTSize)); |
| } |
| |
| bool RealtimeAnalyser::setFftSize(size_t size) { |
| DCHECK(isMainThread()); |
| |
| // Only allow powers of two. |
| unsigned log2size = static_cast<unsigned>(log2(size)); |
| bool isPOT(1UL << log2size == size); |
| |
| if (!isPOT || size > MaxFFTSize || size < MinFFTSize) |
| return false; |
| |
| if (m_fftSize != size) { |
| m_analysisFrame = wrapUnique(new FFTFrame(size)); |
| // m_magnitudeBuffer has size = fftSize / 2 because it contains floats |
| // reduced from complex values in m_analysisFrame. |
| m_magnitudeBuffer.allocate(size / 2); |
| m_fftSize = size; |
| } |
| |
| return true; |
| } |
| |
| void RealtimeAnalyser::writeInput(AudioBus* bus, size_t framesToProcess) { |
| bool isBusGood = bus && bus->numberOfChannels() > 0 && |
| bus->channel(0)->length() >= framesToProcess; |
| DCHECK(isBusGood); |
| if (!isBusGood) |
| return; |
| |
| // FIXME : allow to work with non-FFTSize divisible chunking |
| bool isDestinationGood = |
| m_writeIndex < m_inputBuffer.size() && |
| m_writeIndex + framesToProcess <= m_inputBuffer.size(); |
| DCHECK(isDestinationGood); |
| if (!isDestinationGood) |
| return; |
| |
| // Perform real-time analysis |
| float* dest = m_inputBuffer.data() + m_writeIndex; |
| |
| // Clear the bus and downmix the input according to the down mixing rules. |
| // Then save the result in the m_inputBuffer at the appropriate place. |
| m_downMixBus->zero(); |
| m_downMixBus->sumFrom(*bus); |
| memcpy(dest, m_downMixBus->channel(0)->data(), |
| framesToProcess * sizeof(*dest)); |
| |
| m_writeIndex += framesToProcess; |
| if (m_writeIndex >= InputBufferSize) |
| m_writeIndex = 0; |
| } |
| |
| namespace { |
| |
| void applyWindow(float* p, size_t n) { |
| DCHECK(isMainThread()); |
| |
| // Blackman window |
| double alpha = 0.16; |
| double a0 = 0.5 * (1 - alpha); |
| double a1 = 0.5; |
| double a2 = 0.5 * alpha; |
| |
| for (unsigned i = 0; i < n; ++i) { |
| double x = static_cast<double>(i) / static_cast<double>(n); |
| double window = |
| a0 - a1 * cos(twoPiDouble * x) + a2 * cos(twoPiDouble * 2.0 * x); |
| p[i] *= float(window); |
| } |
| } |
| |
| } // namespace |
| |
| void RealtimeAnalyser::doFFTAnalysis() { |
| DCHECK(isMainThread()); |
| |
| // Unroll the input buffer into a temporary buffer, where we'll apply an |
| // analysis window followed by an FFT. |
| size_t fftSize = this->fftSize(); |
| |
| AudioFloatArray temporaryBuffer(fftSize); |
| float* inputBuffer = m_inputBuffer.data(); |
| float* tempP = temporaryBuffer.data(); |
| |
| // Take the previous fftSize values from the input buffer and copy into the |
| // temporary buffer. |
| unsigned writeIndex = m_writeIndex; |
| if (writeIndex < fftSize) { |
| memcpy(tempP, inputBuffer + writeIndex - fftSize + InputBufferSize, |
| sizeof(*tempP) * (fftSize - writeIndex)); |
| memcpy(tempP + fftSize - writeIndex, inputBuffer, |
| sizeof(*tempP) * writeIndex); |
| } else { |
| memcpy(tempP, inputBuffer + writeIndex - fftSize, sizeof(*tempP) * fftSize); |
| } |
| |
| // Window the input samples. |
| applyWindow(tempP, fftSize); |
| |
| // Do the analysis. |
| m_analysisFrame->doFFT(tempP); |
| |
| float* realP = m_analysisFrame->realData(); |
| float* imagP = m_analysisFrame->imagData(); |
| |
| // Blow away the packed nyquist component. |
| imagP[0] = 0; |
| |
| // Normalize so than an input sine wave at 0dBfs registers as 0dBfs (undo FFT |
| // scaling factor). |
| const double magnitudeScale = 1.0 / fftSize; |
| |
| // A value of 0 does no averaging with the previous result. Larger values |
| // produce slower, but smoother changes. |
| double k = m_smoothingTimeConstant; |
| k = std::max(0.0, k); |
| k = std::min(1.0, k); |
| |
| // Convert the analysis data from complex to magnitude and average with the |
| // previous result. |
| float* destination = magnitudeBuffer().data(); |
| size_t n = magnitudeBuffer().size(); |
| for (size_t i = 0; i < n; ++i) { |
| std::complex<double> c(realP[i], imagP[i]); |
| double scalarMagnitude = abs(c) * magnitudeScale; |
| destination[i] = float(k * destination[i] + (1 - k) * scalarMagnitude); |
| } |
| } |
| |
| void RealtimeAnalyser::convertFloatToDb(DOMFloat32Array* destinationArray) { |
| // Convert from linear magnitude to floating-point decibels. |
| unsigned sourceLength = magnitudeBuffer().size(); |
| size_t len = std::min(sourceLength, destinationArray->length()); |
| if (len > 0) { |
| const float* source = magnitudeBuffer().data(); |
| float* destination = destinationArray->data(); |
| |
| for (unsigned i = 0; i < len; ++i) { |
| float linearValue = source[i]; |
| double dbMag = AudioUtilities::linearToDecibels(linearValue); |
| destination[i] = float(dbMag); |
| } |
| } |
| } |
| |
| void RealtimeAnalyser::getFloatFrequencyData(DOMFloat32Array* destinationArray, |
| double currentTime) { |
| DCHECK(isMainThread()); |
| DCHECK(destinationArray); |
| |
| if (currentTime <= m_lastAnalysisTime) { |
| convertFloatToDb(destinationArray); |
| return; |
| } |
| |
| // Time has advanced since the last call; update the FFT data. |
| m_lastAnalysisTime = currentTime; |
| doFFTAnalysis(); |
| |
| convertFloatToDb(destinationArray); |
| } |
| |
| void RealtimeAnalyser::convertToByteData(DOMUint8Array* destinationArray) { |
| // Convert from linear magnitude to unsigned-byte decibels. |
| unsigned sourceLength = magnitudeBuffer().size(); |
| size_t len = std::min(sourceLength, destinationArray->length()); |
| if (len > 0) { |
| const double rangeScaleFactor = m_maxDecibels == m_minDecibels |
| ? 1 |
| : 1 / (m_maxDecibels - m_minDecibels); |
| const double minDecibels = m_minDecibels; |
| |
| const float* source = magnitudeBuffer().data(); |
| unsigned char* destination = destinationArray->data(); |
| |
| for (unsigned i = 0; i < len; ++i) { |
| float linearValue = source[i]; |
| double dbMag = AudioUtilities::linearToDecibels(linearValue); |
| |
| // The range m_minDecibels to m_maxDecibels will be scaled to byte values |
| // from 0 to UCHAR_MAX. |
| double scaledValue = UCHAR_MAX * (dbMag - minDecibels) * rangeScaleFactor; |
| |
| // Clip to valid range. |
| if (scaledValue < 0) |
| scaledValue = 0; |
| if (scaledValue > UCHAR_MAX) |
| scaledValue = UCHAR_MAX; |
| |
| destination[i] = static_cast<unsigned char>(scaledValue); |
| } |
| } |
| } |
| |
| void RealtimeAnalyser::getByteFrequencyData(DOMUint8Array* destinationArray, |
| double currentTime) { |
| DCHECK(isMainThread()); |
| DCHECK(destinationArray); |
| |
| if (currentTime <= m_lastAnalysisTime) { |
| // FIXME: Is it worth caching the data so we don't have to do the conversion |
| // every time? Perhaps not, since we expect many calls in the same |
| // rendering quantum. |
| convertToByteData(destinationArray); |
| return; |
| } |
| |
| // Time has advanced since the last call; update the FFT data. |
| m_lastAnalysisTime = currentTime; |
| doFFTAnalysis(); |
| |
| convertToByteData(destinationArray); |
| } |
| |
| void RealtimeAnalyser::getFloatTimeDomainData( |
| DOMFloat32Array* destinationArray) { |
| DCHECK(isMainThread()); |
| DCHECK(destinationArray); |
| |
| unsigned fftSize = this->fftSize(); |
| size_t len = std::min(fftSize, destinationArray->length()); |
| if (len > 0) { |
| bool isInputBufferGood = m_inputBuffer.size() == InputBufferSize && |
| m_inputBuffer.size() > fftSize; |
| DCHECK(isInputBufferGood); |
| if (!isInputBufferGood) |
| return; |
| |
| float* inputBuffer = m_inputBuffer.data(); |
| float* destination = destinationArray->data(); |
| |
| unsigned writeIndex = m_writeIndex; |
| |
| for (unsigned i = 0; i < len; ++i) { |
| // Buffer access is protected due to modulo operation. |
| float value = inputBuffer[(i + writeIndex - fftSize + InputBufferSize) % |
| InputBufferSize]; |
| |
| destination[i] = value; |
| } |
| } |
| } |
| |
| void RealtimeAnalyser::getByteTimeDomainData(DOMUint8Array* destinationArray) { |
| DCHECK(isMainThread()); |
| DCHECK(destinationArray); |
| |
| unsigned fftSize = this->fftSize(); |
| size_t len = std::min(fftSize, destinationArray->length()); |
| if (len > 0) { |
| bool isInputBufferGood = m_inputBuffer.size() == InputBufferSize && |
| m_inputBuffer.size() > fftSize; |
| DCHECK(isInputBufferGood); |
| if (!isInputBufferGood) |
| return; |
| |
| float* inputBuffer = m_inputBuffer.data(); |
| unsigned char* destination = destinationArray->data(); |
| |
| unsigned writeIndex = m_writeIndex; |
| |
| for (unsigned i = 0; i < len; ++i) { |
| // Buffer access is protected due to modulo operation. |
| float value = inputBuffer[(i + writeIndex - fftSize + InputBufferSize) % |
| InputBufferSize]; |
| |
| // Scale from nominal -1 -> +1 to unsigned byte. |
| double scaledValue = 128 * (value + 1); |
| |
| // Clip to valid range. |
| if (scaledValue < 0) |
| scaledValue = 0; |
| if (scaledValue > UCHAR_MAX) |
| scaledValue = UCHAR_MAX; |
| |
| destination[i] = static_cast<unsigned char>(scaledValue); |
| } |
| } |
| } |
| |
| } // namespace blink |