| /* |
| * 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 "bindings/core/v8/ExceptionMessages.h" |
| #include "bindings/core/v8/ExceptionState.h" |
| #include "core/dom/ExceptionCode.h" |
| #include "core/frame/UseCounter.h" |
| #include "modules/webaudio/AudioBufferSourceNode.h" |
| #include "modules/webaudio/AudioBufferSourceOptions.h" |
| #include "modules/webaudio/AudioNodeOutput.h" |
| #include "modules/webaudio/BaseAudioContext.h" |
| #include "platform/audio/AudioUtilities.h" |
| #include "wtf/MathExtras.h" |
| #include "wtf/PtrUtil.h" |
| #include <algorithm> |
| |
| namespace blink { |
| |
| const double DefaultGrainDuration = 0.020; // 20ms |
| |
| // Arbitrary upper limit on playback rate. |
| // Higher than expected rates can be useful when playing back oversampled |
| // buffers to minimize linear interpolation aliasing. |
| const double MaxRate = 1024; |
| |
| // Number of extra frames to use when determining if a source node can be |
| // stopped. This should be at least one rendering quantum, but we add one more |
| // quantum for good measure. This doesn't need to be extra precise, just more |
| // than one rendering quantum. See |handleStoppableSourceNode()|. |
| // FIXME: Expose the rendering quantum somehow instead of hardwiring a value |
| // here. |
| const int kExtraStopFrames = 256; |
| |
| AudioBufferSourceHandler::AudioBufferSourceHandler( |
| AudioNode& node, |
| float sampleRate, |
| AudioParamHandler& playbackRate, |
| AudioParamHandler& detune) |
| : AudioScheduledSourceHandler(NodeTypeAudioBufferSource, node, sampleRate), |
| m_buffer(nullptr), |
| m_playbackRate(playbackRate), |
| m_detune(detune), |
| m_isLooping(false), |
| m_didSetLooping(false), |
| m_loopStart(0), |
| m_loopEnd(0), |
| m_virtualReadIndex(0), |
| m_isGrain(false), |
| m_grainOffset(0.0), |
| m_grainDuration(DefaultGrainDuration), |
| m_minPlaybackRate(1.0) { |
| // Default to mono. A call to setBuffer() will set the number of output |
| // channels to that of the buffer. |
| addOutput(1); |
| |
| initialize(); |
| } |
| |
| PassRefPtr<AudioBufferSourceHandler> AudioBufferSourceHandler::create( |
| AudioNode& node, |
| float sampleRate, |
| AudioParamHandler& playbackRate, |
| AudioParamHandler& detune) { |
| return adoptRef( |
| new AudioBufferSourceHandler(node, sampleRate, playbackRate, detune)); |
| } |
| |
| AudioBufferSourceHandler::~AudioBufferSourceHandler() { |
| uninitialize(); |
| } |
| |
| void AudioBufferSourceHandler::process(size_t framesToProcess) { |
| AudioBus* outputBus = output(0).bus(); |
| |
| if (!isInitialized()) { |
| outputBus->zero(); |
| return; |
| } |
| |
| // The audio thread can't block on this lock, so we call tryLock() instead. |
| MutexTryLocker tryLocker(m_processLock); |
| if (tryLocker.locked()) { |
| if (!buffer()) { |
| outputBus->zero(); |
| return; |
| } |
| |
| // After calling setBuffer() with a buffer having a different number of |
| // channels, there can in rare cases be a slight delay before the output bus |
| // is updated to the new number of channels because of use of tryLocks() in |
| // the context's updating system. In this case, if the the buffer has just |
| // been changed and we're not quite ready yet, then just output silence. |
| if (numberOfChannels() != buffer()->numberOfChannels()) { |
| outputBus->zero(); |
| return; |
| } |
| |
| size_t quantumFrameOffset; |
| size_t bufferFramesToProcess; |
| |
| updateSchedulingInfo(framesToProcess, outputBus, quantumFrameOffset, |
| bufferFramesToProcess); |
| |
| if (!bufferFramesToProcess) { |
| outputBus->zero(); |
| return; |
| } |
| |
| for (unsigned i = 0; i < outputBus->numberOfChannels(); ++i) |
| m_destinationChannels[i] = outputBus->channel(i)->mutableData(); |
| |
| // Render by reading directly from the buffer. |
| if (!renderFromBuffer(outputBus, quantumFrameOffset, |
| bufferFramesToProcess)) { |
| outputBus->zero(); |
| return; |
| } |
| |
| outputBus->clearSilentFlag(); |
| } else { |
| // Too bad - the tryLock() failed. We must be in the middle of changing |
| // buffers and were already outputting silence anyway. |
| outputBus->zero(); |
| } |
| } |
| |
| // Returns true if we're finished. |
| bool AudioBufferSourceHandler::renderSilenceAndFinishIfNotLooping( |
| AudioBus*, |
| unsigned index, |
| size_t framesToProcess) { |
| if (!loop()) { |
| // If we're not looping, then stop playing when we get to the end. |
| |
| if (framesToProcess > 0) { |
| // We're not looping and we've reached the end of the sample data, but we |
| // still need to provide more output, so generate silence for the |
| // remaining. |
| for (unsigned i = 0; i < numberOfChannels(); ++i) |
| memset(m_destinationChannels[i] + index, 0, |
| sizeof(float) * framesToProcess); |
| } |
| |
| finish(); |
| return true; |
| } |
| return false; |
| } |
| |
| bool AudioBufferSourceHandler::renderFromBuffer(AudioBus* bus, |
| unsigned destinationFrameOffset, |
| size_t numberOfFrames) { |
| DCHECK(context()->isAudioThread()); |
| |
| // Basic sanity checking |
| DCHECK(bus); |
| DCHECK(buffer()); |
| if (!bus || !buffer()) |
| return false; |
| |
| unsigned numberOfChannels = this->numberOfChannels(); |
| unsigned busNumberOfChannels = bus->numberOfChannels(); |
| |
| bool channelCountGood = |
| numberOfChannels && numberOfChannels == busNumberOfChannels; |
| DCHECK(channelCountGood); |
| if (!channelCountGood) |
| return false; |
| |
| // Sanity check destinationFrameOffset, numberOfFrames. |
| size_t destinationLength = bus->length(); |
| |
| bool isLengthGood = |
| destinationLength <= AudioUtilities::kRenderQuantumFrames && |
| numberOfFrames <= AudioUtilities::kRenderQuantumFrames; |
| DCHECK(isLengthGood); |
| if (!isLengthGood) |
| return false; |
| |
| bool isOffsetGood = |
| destinationFrameOffset <= destinationLength && |
| destinationFrameOffset + numberOfFrames <= destinationLength; |
| DCHECK(isOffsetGood); |
| if (!isOffsetGood) |
| return false; |
| |
| // Potentially zero out initial frames leading up to the offset. |
| if (destinationFrameOffset) { |
| for (unsigned i = 0; i < numberOfChannels; ++i) |
| memset(m_destinationChannels[i], 0, |
| sizeof(float) * destinationFrameOffset); |
| } |
| |
| // Offset the pointers to the correct offset frame. |
| unsigned writeIndex = destinationFrameOffset; |
| |
| size_t bufferLength = buffer()->length(); |
| double bufferSampleRate = buffer()->sampleRate(); |
| |
| // Avoid converting from time to sample-frames twice by computing |
| // the grain end time first before computing the sample frame. |
| unsigned endFrame = |
| m_isGrain ? AudioUtilities::timeToSampleFrame( |
| m_grainOffset + m_grainDuration, bufferSampleRate) |
| : bufferLength; |
| |
| // This is a HACK to allow for HRTF tail-time - avoids glitch at end. |
| // FIXME: implement tailTime for each AudioNode for a more general solution to |
| // this problem, https://bugs.webkit.org/show_bug.cgi?id=77224 |
| if (m_isGrain) |
| endFrame += 512; |
| |
| // Do some sanity checking. |
| if (endFrame > bufferLength) |
| endFrame = bufferLength; |
| |
| // If the .loop attribute is true, then values of |
| // m_loopStart == 0 && m_loopEnd == 0 implies that we should use the entire |
| // buffer as the loop, otherwise use the loop values in m_loopStart and |
| // m_loopEnd. |
| double virtualEndFrame = endFrame; |
| double virtualDeltaFrames = endFrame; |
| |
| if (loop() && (m_loopStart || m_loopEnd) && m_loopStart >= 0 && |
| m_loopEnd > 0 && m_loopStart < m_loopEnd) { |
| // Convert from seconds to sample-frames. |
| double loopStartFrame = m_loopStart * buffer()->sampleRate(); |
| double loopEndFrame = m_loopEnd * buffer()->sampleRate(); |
| |
| virtualEndFrame = std::min(loopEndFrame, virtualEndFrame); |
| virtualDeltaFrames = virtualEndFrame - loopStartFrame; |
| } |
| |
| // If we're looping and the offset (virtualReadIndex) is past the end of the |
| // loop, wrap back to the beginning of the loop. For other cases, nothing |
| // needs to be done. |
| if (loop() && m_virtualReadIndex >= virtualEndFrame) { |
| m_virtualReadIndex = |
| (m_loopStart < 0) ? 0 : (m_loopStart * buffer()->sampleRate()); |
| m_virtualReadIndex = |
| std::min(m_virtualReadIndex, static_cast<double>(bufferLength - 1)); |
| } |
| |
| double computedPlaybackRate = computePlaybackRate(); |
| |
| // Sanity check that our playback rate isn't larger than the loop size. |
| if (computedPlaybackRate > virtualDeltaFrames) |
| return false; |
| |
| // Get local copy. |
| double virtualReadIndex = m_virtualReadIndex; |
| |
| // Render loop - reading from the source buffer to the destination using |
| // linear interpolation. |
| int framesToProcess = numberOfFrames; |
| |
| const float** sourceChannels = m_sourceChannels.get(); |
| float** destinationChannels = m_destinationChannels.get(); |
| |
| DCHECK_GE(virtualReadIndex, 0); |
| DCHECK_GE(virtualDeltaFrames, 0); |
| DCHECK_GE(virtualEndFrame, 0); |
| |
| // Optimize for the very common case of playing back with |
| // computedPlaybackRate == 1. We can avoid the linear interpolation. |
| if (computedPlaybackRate == 1 && |
| virtualReadIndex == floor(virtualReadIndex) && |
| virtualDeltaFrames == floor(virtualDeltaFrames) && |
| virtualEndFrame == floor(virtualEndFrame)) { |
| unsigned readIndex = static_cast<unsigned>(virtualReadIndex); |
| unsigned deltaFrames = static_cast<unsigned>(virtualDeltaFrames); |
| endFrame = static_cast<unsigned>(virtualEndFrame); |
| while (framesToProcess > 0) { |
| int framesToEnd = endFrame - readIndex; |
| int framesThisTime = std::min(framesToProcess, framesToEnd); |
| framesThisTime = std::max(0, framesThisTime); |
| |
| DCHECK_LE(writeIndex + framesThisTime, destinationLength); |
| DCHECK_LE(readIndex + framesThisTime, bufferLength); |
| |
| for (unsigned i = 0; i < numberOfChannels; ++i) |
| memcpy(destinationChannels[i] + writeIndex, |
| sourceChannels[i] + readIndex, sizeof(float) * framesThisTime); |
| |
| writeIndex += framesThisTime; |
| readIndex += framesThisTime; |
| framesToProcess -= framesThisTime; |
| |
| // It can happen that framesThisTime is 0. DCHECK that we will actually |
| // exit the loop in this case. framesThisTime is 0 only if |
| // readIndex >= endFrame; |
| DCHECK(framesThisTime ? true : readIndex >= endFrame); |
| |
| // Wrap-around. |
| if (readIndex >= endFrame) { |
| readIndex -= deltaFrames; |
| if (renderSilenceAndFinishIfNotLooping(bus, writeIndex, |
| framesToProcess)) |
| break; |
| } |
| } |
| virtualReadIndex = readIndex; |
| } else { |
| while (framesToProcess--) { |
| unsigned readIndex = static_cast<unsigned>(virtualReadIndex); |
| double interpolationFactor = virtualReadIndex - readIndex; |
| |
| // For linear interpolation we need the next sample-frame too. |
| unsigned readIndex2 = readIndex + 1; |
| if (readIndex2 >= bufferLength) { |
| if (loop()) { |
| // Make sure to wrap around at the end of the buffer. |
| readIndex2 = |
| static_cast<unsigned>(virtualReadIndex + 1 - virtualDeltaFrames); |
| } else { |
| readIndex2 = readIndex; |
| } |
| } |
| |
| // Final sanity check on buffer access. |
| // FIXME: as an optimization, try to get rid of this inner-loop check and |
| // put assertions and guards before the loop. |
| if (readIndex >= bufferLength || readIndex2 >= bufferLength) |
| break; |
| |
| // Linear interpolation. |
| for (unsigned i = 0; i < numberOfChannels; ++i) { |
| float* destination = destinationChannels[i]; |
| const float* source = sourceChannels[i]; |
| |
| double sample1 = source[readIndex]; |
| double sample2 = source[readIndex2]; |
| double sample = (1.0 - interpolationFactor) * sample1 + |
| interpolationFactor * sample2; |
| |
| destination[writeIndex] = clampTo<float>(sample); |
| } |
| writeIndex++; |
| |
| virtualReadIndex += computedPlaybackRate; |
| |
| // Wrap-around, retaining sub-sample position since virtualReadIndex is |
| // floating-point. |
| if (virtualReadIndex >= virtualEndFrame) { |
| virtualReadIndex -= virtualDeltaFrames; |
| if (renderSilenceAndFinishIfNotLooping(bus, writeIndex, |
| framesToProcess)) |
| break; |
| } |
| } |
| } |
| |
| bus->clearSilentFlag(); |
| |
| m_virtualReadIndex = virtualReadIndex; |
| |
| return true; |
| } |
| |
| void AudioBufferSourceHandler::setBuffer(AudioBuffer* buffer, |
| ExceptionState& exceptionState) { |
| DCHECK(isMainThread()); |
| |
| if (m_buffer) { |
| exceptionState.throwDOMException( |
| InvalidStateError, |
| "Cannot set buffer after it has been already been set"); |
| return; |
| } |
| |
| // The context must be locked since changing the buffer can re-configure the |
| // number of channels that are output. |
| BaseAudioContext::AutoLocker contextLocker(context()); |
| |
| // This synchronizes with process(). |
| MutexLocker processLocker(m_processLock); |
| |
| if (buffer) { |
| // Do any necesssary re-configuration to the buffer's number of channels. |
| unsigned numberOfChannels = buffer->numberOfChannels(); |
| |
| // This should not be possible since AudioBuffers can't be created with too |
| // many channels either. |
| if (numberOfChannels > BaseAudioContext::maxNumberOfChannels()) { |
| exceptionState.throwDOMException( |
| NotSupportedError, ExceptionMessages::indexOutsideRange( |
| "number of input channels", numberOfChannels, |
| 1u, ExceptionMessages::InclusiveBound, |
| BaseAudioContext::maxNumberOfChannels(), |
| ExceptionMessages::InclusiveBound)); |
| return; |
| } |
| |
| output(0).setNumberOfChannels(numberOfChannels); |
| |
| m_sourceChannels = wrapArrayUnique(new const float*[numberOfChannels]); |
| m_destinationChannels = wrapArrayUnique(new float*[numberOfChannels]); |
| |
| for (unsigned i = 0; i < numberOfChannels; ++i) |
| m_sourceChannels[i] = buffer->getChannelData(i)->data(); |
| |
| // If this is a grain (as set by a previous call to start()), validate the |
| // grain parameters now since it wasn't validated when start was called |
| // (because there was no buffer then). |
| if (m_isGrain) |
| clampGrainParameters(buffer); |
| } |
| |
| m_virtualReadIndex = 0; |
| m_buffer = buffer; |
| } |
| |
| unsigned AudioBufferSourceHandler::numberOfChannels() { |
| return output(0).numberOfChannels(); |
| } |
| |
| void AudioBufferSourceHandler::clampGrainParameters(const AudioBuffer* buffer) { |
| DCHECK(buffer); |
| |
| // We have a buffer so we can clip the offset and duration to lie within the |
| // buffer. |
| double bufferDuration = buffer->duration(); |
| |
| m_grainOffset = clampTo(m_grainOffset, 0.0, bufferDuration); |
| |
| // If the duration was not explicitly given, use the buffer duration to set |
| // the grain duration. Otherwise, we want to use the user-specified value, of |
| // course. |
| if (!m_isDurationGiven) |
| m_grainDuration = bufferDuration - m_grainOffset; |
| |
| if (m_isDurationGiven && loop()) { |
| // We're looping a grain with a grain duration specified. Schedule the loop |
| // to stop after grainDuration seconds after starting, possibly running the |
| // loop multiple times if grainDuration is larger than the buffer duration. |
| // The net effect is as if the user called stop(when + grainDuration). |
| m_grainDuration = |
| clampTo(m_grainDuration, 0.0, std::numeric_limits<double>::infinity()); |
| m_endTime = m_startTime + m_grainDuration; |
| } else { |
| m_grainDuration = |
| clampTo(m_grainDuration, 0.0, bufferDuration - m_grainOffset); |
| } |
| |
| // We call timeToSampleFrame here since at playbackRate == 1 we don't want to |
| // go through linear interpolation at a sub-sample position since it will |
| // degrade the quality. When aligned to the sample-frame the playback will be |
| // identical to the PCM data stored in the buffer. Since playbackRate == 1 is |
| // very common, it's worth considering quality. |
| m_virtualReadIndex = |
| AudioUtilities::timeToSampleFrame(m_grainOffset, buffer->sampleRate()); |
| } |
| |
| void AudioBufferSourceHandler::start(double when, |
| ExceptionState& exceptionState) { |
| AudioScheduledSourceHandler::start(when, exceptionState); |
| } |
| |
| void AudioBufferSourceHandler::start(double when, |
| double grainOffset, |
| ExceptionState& exceptionState) { |
| startSource(when, grainOffset, buffer() ? buffer()->duration() : 0, false, |
| exceptionState); |
| } |
| |
| void AudioBufferSourceHandler::start(double when, |
| double grainOffset, |
| double grainDuration, |
| ExceptionState& exceptionState) { |
| startSource(when, grainOffset, grainDuration, true, exceptionState); |
| } |
| |
| void AudioBufferSourceHandler::startSource(double when, |
| double grainOffset, |
| double grainDuration, |
| bool isDurationGiven, |
| ExceptionState& exceptionState) { |
| DCHECK(isMainThread()); |
| |
| // TODO(mlamouri): record when this is called with a user gesture and could |
| // have started the AudioContext if following Safari iOS rules. |
| |
| if (playbackState() != UNSCHEDULED_STATE) { |
| exceptionState.throwDOMException(InvalidStateError, |
| "cannot call start more than once."); |
| return; |
| } |
| |
| if (when < 0) { |
| exceptionState.throwDOMException( |
| InvalidStateError, |
| ExceptionMessages::indexExceedsMinimumBound("start time", when, 0.0)); |
| return; |
| } |
| |
| if (grainOffset < 0) { |
| exceptionState.throwDOMException( |
| InvalidStateError, ExceptionMessages::indexExceedsMinimumBound( |
| "offset", grainOffset, 0.0)); |
| return; |
| } |
| |
| if (grainDuration < 0) { |
| exceptionState.throwDOMException( |
| InvalidStateError, ExceptionMessages::indexExceedsMinimumBound( |
| "duration", grainDuration, 0.0)); |
| return; |
| } |
| |
| // The node is started. Add a reference to keep us alive so that audio |
| // will eventually get played even if Javascript should drop all references |
| // to this node. The reference will get dropped when the source has finished |
| // playing. |
| context()->notifySourceNodeStartedProcessing(node()); |
| |
| // This synchronizes with process(). updateSchedulingInfo will read some of |
| // the variables being set here. |
| MutexLocker processLocker(m_processLock); |
| |
| m_isDurationGiven = isDurationGiven; |
| m_isGrain = true; |
| m_grainOffset = grainOffset; |
| m_grainDuration = grainDuration; |
| |
| // If |when| < currentTime, the source must start now according to the spec. |
| // So just set startTime to currentTime in this case to start the source now. |
| m_startTime = std::max(when, context()->currentTime()); |
| |
| if (buffer()) |
| clampGrainParameters(buffer()); |
| |
| setPlaybackState(SCHEDULED_STATE); |
| } |
| |
| double AudioBufferSourceHandler::computePlaybackRate() { |
| // Incorporate buffer's sample-rate versus BaseAudioContext's sample-rate. |
| // Normally it's not an issue because buffers are loaded at the |
| // BaseAudioContext's sample-rate, but we can handle it in any case. |
| double sampleRateFactor = 1.0; |
| if (buffer()) { |
| // Use doubles to compute this to full accuracy. |
| sampleRateFactor = |
| buffer()->sampleRate() / static_cast<double>(sampleRate()); |
| } |
| |
| // Use finalValue() to incorporate changes of AudioParamTimeline and |
| // AudioSummingJunction from m_playbackRate AudioParam. |
| double basePlaybackRate = m_playbackRate->finalValue(); |
| |
| double finalPlaybackRate = sampleRateFactor * basePlaybackRate; |
| |
| // Take the detune value into account for the final playback rate. |
| finalPlaybackRate *= pow(2, m_detune->finalValue() / 1200); |
| |
| // Sanity check the total rate. It's very important that the resampler not |
| // get any bad rate values. |
| finalPlaybackRate = clampTo(finalPlaybackRate, 0.0, MaxRate); |
| |
| bool isPlaybackRateValid = |
| !std::isnan(finalPlaybackRate) && !std::isinf(finalPlaybackRate); |
| DCHECK(isPlaybackRateValid); |
| |
| if (!isPlaybackRateValid) |
| finalPlaybackRate = 1.0; |
| |
| // Record the minimum playback rate for use by handleStoppableSourceNode. |
| m_minPlaybackRate = std::min(finalPlaybackRate, m_minPlaybackRate); |
| |
| return finalPlaybackRate; |
| } |
| |
| bool AudioBufferSourceHandler::propagatesSilence() const { |
| return !isPlayingOrScheduled() || hasFinished() || !m_buffer; |
| } |
| |
| void AudioBufferSourceHandler::handleStoppableSourceNode() { |
| // If the source node is not looping, and we have a buffer, we can determine |
| // when the source would stop playing. This is intended to handle the |
| // (uncommon) scenario where start() has been called but is never connected to |
| // the destination (directly or indirectly). By stopping the node, the node |
| // can be collected. Otherwise, the node will never get collected, leaking |
| // memory. |
| // |
| // If looping was ever done (m_didSetLooping = true), give up. We can't |
| // easily determine how long we looped so we don't know the actual duration |
| // thus far, so don't try to do anything fancy. |
| if (!m_didSetLooping && buffer() && isPlayingOrScheduled() && |
| m_minPlaybackRate > 0) { |
| // Adjust the duration to include the playback rate. Only need to account |
| // for rate < 1 which makes the sound last longer. For rate >= 1, the |
| // source stops sooner, but that's ok. |
| double actualDuration = buffer()->duration() / m_minPlaybackRate; |
| |
| double stopTime = m_startTime + actualDuration; |
| |
| // See crbug.com/478301. If a source node is started via start(), the source |
| // may not start at that time but one quantum (128 frames) later. But we |
| // compute the stop time based on the start time and the duration, so we end |
| // up stopping one quantum early. Thus, add a little extra time; we just |
| // need to stop the source sometime after it should have stopped if it |
| // hadn't already. We don't need to be super precise on when to stop. |
| double extraStopTime = |
| kExtraStopFrames / static_cast<double>(context()->sampleRate()); |
| |
| stopTime += extraStopTime; |
| if (context()->currentTime() > stopTime) { |
| // The context time has passed the time when the source nodes should have |
| // stopped playing. Stop the node now and deref it. (But don't run the |
| // onEnded event because the source never actually played.) |
| finishWithoutOnEnded(); |
| } |
| } |
| } |
| |
| // ---------------------------------------------------------------- |
| AudioBufferSourceNode::AudioBufferSourceNode(BaseAudioContext& context) |
| : AudioScheduledSourceNode(context), |
| m_playbackRate(AudioParam::create(context, |
| ParamTypeAudioBufferSourcePlaybackRate, |
| 1.0)), |
| m_detune( |
| AudioParam::create(context, ParamTypeAudioBufferSourceDetune, 0.0)) { |
| setHandler(AudioBufferSourceHandler::create(*this, context.sampleRate(), |
| m_playbackRate->handler(), |
| m_detune->handler())); |
| } |
| |
| AudioBufferSourceNode* AudioBufferSourceNode::create( |
| BaseAudioContext& context, |
| ExceptionState& exceptionState) { |
| DCHECK(isMainThread()); |
| |
| if (context.isContextClosed()) { |
| context.throwExceptionForClosedState(exceptionState); |
| return nullptr; |
| } |
| |
| return new AudioBufferSourceNode(context); |
| } |
| |
| AudioBufferSourceNode* AudioBufferSourceNode::create( |
| BaseAudioContext* context, |
| AudioBufferSourceOptions& options, |
| ExceptionState& exceptionState) { |
| DCHECK(isMainThread()); |
| |
| AudioBufferSourceNode* node = create(*context, exceptionState); |
| |
| if (!node) |
| return nullptr; |
| |
| if (options.hasBuffer()) |
| node->setBuffer(options.buffer(), exceptionState); |
| if (options.hasDetune()) |
| node->detune()->setValue(options.detune()); |
| if (options.hasLoop()) |
| node->setLoop(options.loop()); |
| if (options.hasLoopEnd()) |
| node->setLoopEnd(options.loopEnd()); |
| if (options.hasLoopStart()) |
| node->setLoopStart(options.loopStart()); |
| if (options.hasPlaybackRate()) |
| node->playbackRate()->setValue(options.playbackRate()); |
| |
| return node; |
| } |
| |
| DEFINE_TRACE(AudioBufferSourceNode) { |
| visitor->trace(m_playbackRate); |
| visitor->trace(m_detune); |
| AudioScheduledSourceNode::trace(visitor); |
| } |
| |
| AudioBufferSourceHandler& AudioBufferSourceNode::audioBufferSourceHandler() |
| const { |
| return static_cast<AudioBufferSourceHandler&>(handler()); |
| } |
| |
| AudioBuffer* AudioBufferSourceNode::buffer() const { |
| return audioBufferSourceHandler().buffer(); |
| } |
| |
| void AudioBufferSourceNode::setBuffer(AudioBuffer* newBuffer, |
| ExceptionState& exceptionState) { |
| audioBufferSourceHandler().setBuffer(newBuffer, exceptionState); |
| } |
| |
| AudioParam* AudioBufferSourceNode::playbackRate() const { |
| return m_playbackRate; |
| } |
| |
| AudioParam* AudioBufferSourceNode::detune() const { |
| return m_detune; |
| } |
| |
| bool AudioBufferSourceNode::loop() const { |
| return audioBufferSourceHandler().loop(); |
| } |
| |
| void AudioBufferSourceNode::setLoop(bool loop) { |
| audioBufferSourceHandler().setLoop(loop); |
| } |
| |
| double AudioBufferSourceNode::loopStart() const { |
| return audioBufferSourceHandler().loopStart(); |
| } |
| |
| void AudioBufferSourceNode::setLoopStart(double loopStart) { |
| audioBufferSourceHandler().setLoopStart(loopStart); |
| } |
| |
| double AudioBufferSourceNode::loopEnd() const { |
| return audioBufferSourceHandler().loopEnd(); |
| } |
| |
| void AudioBufferSourceNode::setLoopEnd(double loopEnd) { |
| audioBufferSourceHandler().setLoopEnd(loopEnd); |
| } |
| |
| void AudioBufferSourceNode::start(ExceptionState& exceptionState) { |
| audioBufferSourceHandler().start(0, exceptionState); |
| } |
| |
| void AudioBufferSourceNode::start(double when, ExceptionState& exceptionState) { |
| audioBufferSourceHandler().start(when, exceptionState); |
| } |
| |
| void AudioBufferSourceNode::start(double when, |
| double grainOffset, |
| ExceptionState& exceptionState) { |
| audioBufferSourceHandler().start(when, grainOffset, exceptionState); |
| } |
| |
| void AudioBufferSourceNode::start(double when, |
| double grainOffset, |
| double grainDuration, |
| ExceptionState& exceptionState) { |
| audioBufferSourceHandler().start(when, grainOffset, grainDuration, |
| exceptionState); |
| } |
| |
| } // namespace blink |