blob: 19240a6d6e73163992f921cfe53373a36f2a0377 [file] [log] [blame]
/*
* 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