2019-07-23 15:27:55 +00:00
import { ToneAudioBuffer } from "../../core/context/ToneAudioBuffer" ;
2019-12-14 21:09:24 +00:00
import { Positive , Seconds , Time } from "../../core/type/Units" ;
2019-07-23 15:27:55 +00:00
import { defaultArg , optionsFromArguments } from "../../core/util/Defaults" ;
import { noOp } from "../../core/util/Interface" ;
import { isUndef } from "../../core/util/TypeCheck" ;
import { Source , SourceOptions } from "../Source" ;
2019-09-04 22:39:28 +00:00
import { ToneBufferSource } from "./ToneBufferSource" ;
2019-11-19 20:44:44 +00:00
import { assertRange } from "../../core/util/Debug" ;
2019-12-17 17:42:40 +00:00
import { timeRange } from "../../core/util/Decorator" ;
2019-07-23 15:27:55 +00:00
2019-09-04 22:34:42 +00:00
export interface PlayerOptions extends SourceOptions {
2019-07-23 15:27:55 +00:00
onload : ( ) = > void ;
2020-01-30 04:34:05 +00:00
onerror : ( error : Error ) = > void ;
2019-07-23 15:27:55 +00:00
playbackRate : Positive ;
loop : boolean ;
autostart : boolean ;
loopStart : Time ;
loopEnd : Time ;
reverse : boolean ;
fadeIn : Time ;
fadeOut : Time ;
url? : ToneAudioBuffer | string | AudioBuffer ;
}
/ * *
* Player is an audio file player with start , loop , and stop functions .
* @example
2020-06-08 00:14:48 +00:00
* const player = new Tone . Player ( "https://tonejs.github.io/audio/berklee/gong_1.mp3" ) . toDestination ( ) ;
2019-10-25 20:54:33 +00:00
* // play as soon as the buffer is loaded
2019-07-23 15:27:55 +00:00
* player . autostart = true ;
2019-09-16 14:15:23 +00:00
* @category Source
2019-07-23 15:27:55 +00:00
* /
export class Player extends Source < PlayerOptions > {
2019-09-04 23:18:44 +00:00
readonly name : string = "Player" ;
2019-07-23 15:27:55 +00:00
/ * *
* If the file should play as soon
* as the buffer is loaded .
* /
autostart : boolean ;
/ * *
2019-09-14 20:39:18 +00:00
* The buffer
2019-07-23 15:27:55 +00:00
* /
private _buffer : ToneAudioBuffer ;
/ * *
2019-09-14 20:39:18 +00:00
* if the buffer should loop once it ' s over
2019-07-23 15:27:55 +00:00
* /
private _loop : boolean ;
/ * *
2019-09-14 20:39:18 +00:00
* if 'loop' is true , the loop will start at this position
2019-07-23 15:27:55 +00:00
* /
private _loopStart : Time ;
/ * *
2019-09-14 20:39:18 +00:00
* if 'loop' is true , the loop will end at this position
2019-07-23 15:27:55 +00:00
* /
private _loopEnd : Time ;
/ * *
2019-09-14 20:39:18 +00:00
* the playback rate
2019-07-23 15:27:55 +00:00
* /
private _playbackRate : Positive ;
/ * *
2019-09-14 20:39:18 +00:00
* All of the active buffer source nodes
2019-07-23 15:27:55 +00:00
* /
2019-07-23 17:47:36 +00:00
private _activeSources : Set < ToneBufferSource > = new Set ( ) ;
2019-07-23 15:27:55 +00:00
/ * *
2019-09-14 20:39:18 +00:00
* The fadeIn time of the amplitude envelope .
2019-07-23 15:27:55 +00:00
* /
2019-12-17 17:42:40 +00:00
@timeRange ( 0 )
2019-07-23 15:27:55 +00:00
fadeIn : Time ;
2020-04-17 01:46:55 +00:00
2019-07-23 15:27:55 +00:00
/ * *
2019-09-14 20:39:18 +00:00
* The fadeOut time of the amplitude envelope .
2019-07-23 15:27:55 +00:00
* /
2019-12-17 17:42:40 +00:00
@timeRange ( 0 )
2019-07-23 15:27:55 +00:00
fadeOut : Time ;
2019-08-27 15:47:52 +00:00
/ * *
* @param url Either the AudioBuffer or the url from which to load the AudioBuffer
* @param onload The function to invoke when the buffer is loaded .
* /
2021-12-20 16:59:20 +00:00
constructor (
url? : string | AudioBuffer | ToneAudioBuffer ,
onload ? : ( ) = > void
) ;
2019-08-27 15:47:52 +00:00
constructor ( options? : Partial < PlayerOptions > ) ;
2019-07-23 15:27:55 +00:00
constructor ( ) {
2021-12-20 16:59:20 +00:00
super (
optionsFromArguments ( Player . getDefaults ( ) , arguments , [
"url" ,
"onload" ,
] )
) ;
const options = optionsFromArguments ( Player . getDefaults ( ) , arguments , [
"url" ,
"onload" ,
] ) ;
2019-07-23 15:27:55 +00:00
this . _buffer = new ToneAudioBuffer ( {
onload : this._onload.bind ( this , options . onload ) ,
2020-01-30 04:34:05 +00:00
onerror : options.onerror ,
2019-07-23 15:27:55 +00:00
reverse : options.reverse ,
url : options.url ,
} ) ;
this . autostart = options . autostart ;
this . _loop = options . loop ;
this . _loopStart = options . loopStart ;
this . _loopEnd = options . loopEnd ;
this . _playbackRate = options . playbackRate ;
this . fadeIn = options . fadeIn ;
this . fadeOut = options . fadeOut ;
}
static getDefaults ( ) : PlayerOptions {
return Object . assign ( Source . getDefaults ( ) , {
2019-09-16 03:32:40 +00:00
autostart : false ,
fadeIn : 0 ,
fadeOut : 0 ,
loop : false ,
loopEnd : 0 ,
loopStart : 0 ,
onload : noOp ,
2020-01-30 04:34:05 +00:00
onerror : noOp ,
2019-09-16 03:32:40 +00:00
playbackRate : 1 ,
reverse : false ,
2019-07-23 15:27:55 +00:00
} ) ;
}
/ * *
* Load the audio file as an audio buffer .
* Decodes the audio asynchronously and invokes
* the callback once the audio buffer loads .
* Note : this does not need to be called if a url
* was passed in to the constructor . Only use this
* if you want to manually load a new url .
* @param url The url of the buffer to load . Filetype support depends on the browser .
* /
async load ( url : string ) : Promise < this > {
await this . _buffer . load ( url ) ;
this . _onload ( ) ;
return this ;
}
/ * *
* Internal callback when the buffer is loaded .
* /
private _onload ( callback : ( ) = > void = noOp ) : void {
callback ( ) ;
if ( this . autostart ) {
this . start ( ) ;
}
}
/ * *
* Internal callback when the buffer is done playing .
* /
private _onSourceEnd ( source : ToneBufferSource ) : void {
2019-08-10 03:07:09 +00:00
// invoke the onstop function
this . onstop ( this ) ;
// delete the source from the active sources
2019-07-23 17:47:36 +00:00
this . _activeSources . delete ( source ) ;
2021-12-20 16:59:20 +00:00
if (
this . _activeSources . size === 0 &&
! this . _synced &&
this . _state . getValueAtTime ( this . now ( ) ) === "started"
) {
2020-08-22 14:13:32 +00:00
// remove the 'implicitEnd' event and replace with an explicit end
this . _state . cancel ( this . now ( ) ) ;
2019-07-23 15:27:55 +00:00
this . _state . setStateAtTime ( "stopped" , this . now ( ) ) ;
}
}
/ * *
2019-09-14 20:39:18 +00:00
* Play the buffer at the given startTime . Optionally add an offset
* and / or duration which will play the buffer from a position
* within the buffer for the given duration .
2019-07-23 15:27:55 +00:00
*
2019-08-30 16:06:38 +00:00
* @param time When the player should start .
2019-12-22 05:37:51 +00:00
* @param offset The offset from the beginning of the sample to start at .
2019-10-23 03:04:52 +00:00
* @param duration How long the sample should play . If no duration is given , it will default to the full length of the sample ( minus any offset )
2019-07-23 15:27:55 +00:00
* /
start ( time? : Time , offset? : Time , duration? : Time ) : this {
super . start ( time , offset , duration ) ;
return this ;
}
/ * *
2019-09-14 20:39:18 +00:00
* Internal start method
2019-07-23 15:27:55 +00:00
* /
protected _start ( startTime? : Time , offset? : Time , duration? : Time ) : void {
2019-12-22 05:37:51 +00:00
// if it's a loop the default offset is the loopStart point
2019-07-23 15:27:55 +00:00
if ( this . _loop ) {
offset = defaultArg ( offset , this . _loopStart ) ;
} else {
// otherwise the default offset is 0
offset = defaultArg ( offset , 0 ) ;
}
// compute the values in seconds
2020-09-24 02:11:38 +00:00
const computedOffset = this . toSeconds ( offset ) ;
2019-07-23 15:27:55 +00:00
// compute the duration which is either the passed in duration of the buffer.duration - offset
2019-07-30 19:35:27 +00:00
const origDuration = duration ;
2021-12-20 16:59:20 +00:00
duration = defaultArg (
duration ,
Math . max ( this . _buffer . duration - computedOffset , 0 )
) ;
2019-07-30 19:35:27 +00:00
let computedDuration = this . toSeconds ( duration ) ;
2019-07-23 15:27:55 +00:00
// scale it by the playback rate
computedDuration = computedDuration / this . _playbackRate ;
// get the start time
startTime = this . toSeconds ( startTime ) ;
// make the source
const source = new ToneBufferSource ( {
2020-01-30 04:34:05 +00:00
url : this._buffer ,
2019-07-23 15:27:55 +00:00
context : this.context ,
2019-09-16 03:32:40 +00:00
fadeIn : this.fadeIn ,
fadeOut : this.fadeOut ,
loop : this._loop ,
loopEnd : this._loopEnd ,
loopStart : this._loopStart ,
onended : this._onSourceEnd.bind ( this ) ,
playbackRate : this._playbackRate ,
2019-07-23 15:27:55 +00:00
} ) . connect ( this . output ) ;
// set the looping properties
if ( ! this . _loop && ! this . _synced ) {
2019-11-18 19:48:24 +00:00
// cancel the previous stop
this . _state . cancel ( startTime + computedDuration ) ;
2019-07-23 15:27:55 +00:00
// if it's not looping, set the state change at the end of the sample
2021-12-20 16:59:20 +00:00
this . _state . setStateAtTime (
"stopped" ,
startTime + computedDuration ,
{
implicitEnd : true ,
}
) ;
2019-07-23 15:27:55 +00:00
}
// add it to the array of active sources
2019-07-23 17:47:36 +00:00
this . _activeSources . add ( source ) ;
2019-07-23 15:27:55 +00:00
// start it
2019-07-30 19:35:27 +00:00
if ( this . _loop && isUndef ( origDuration ) ) {
2019-11-18 19:48:24 +00:00
source . start ( startTime , computedOffset ) ;
2019-07-23 15:27:55 +00:00
} else {
// subtract the fade out time
2021-12-20 16:59:20 +00:00
source . start (
startTime ,
computedOffset ,
computedDuration - this . toSeconds ( this . fadeOut )
) ;
2019-07-23 15:27:55 +00:00
}
}
/ * *
2019-09-14 20:39:18 +00:00
* Stop playback .
2019-07-23 15:27:55 +00:00
* /
protected _stop ( time? : Time ) : void {
const computedTime = this . toSeconds ( time ) ;
2021-12-20 16:59:20 +00:00
this . _activeSources . forEach ( ( source ) = > source . stop ( computedTime ) ) ;
2019-07-23 15:27:55 +00:00
}
/ * *
* Stop and then restart the player from the beginning ( or offset )
* @param time When the player should start .
* @param offset The offset from the beginning of the sample to start at .
2021-12-20 16:59:20 +00:00
* @param duration How long the sample should play . If no duration is given ,
2019-12-14 21:09:24 +00:00
* it will default to the full length of the sample ( minus any offset )
2019-07-23 15:27:55 +00:00
* /
2019-12-14 21:09:24 +00:00
restart ( time? : Seconds , offset? : Time , duration? : Time ) : this {
super . restart ( time , offset , duration ) ;
2019-07-23 15:27:55 +00:00
return this ;
}
2020-04-17 01:46:55 +00:00
2019-12-14 21:09:24 +00:00
protected _restart ( time? : Seconds , offset? : Time , duration? : Time ) : void {
2021-10-30 02:27:55 +00:00
[ . . . this . _activeSources ] . pop ( ) ? . stop ( time ) ; // explicitly stop only the most recently created source, to avoid edge case when > 1 source exists and _stop() erroneously sets all stop times past original end offset
2019-12-14 21:09:24 +00:00
this . _start ( time , offset , duration ) ;
}
2019-07-23 15:27:55 +00:00
/ * *
2019-09-14 20:39:18 +00:00
* Seek to a specific time in the player ' s buffer . If the
* source is no longer playing at that time , it will stop .
2019-10-23 03:04:52 +00:00
* @param offset The time to seek to .
* @param when The time for the seek event to occur .
2019-08-30 16:06:38 +00:00
* @example
2020-06-08 00:14:48 +00:00
* const player = new Tone . Player ( "https://tonejs.github.io/audio/berklee/gurgling_theremin_1.mp3" , ( ) = > {
2019-10-25 20:54:33 +00:00
* player . start ( ) ;
* // seek to the offset in 1 second from now
* player . seek ( 0.4 , "+1" ) ;
* } ) . toDestination ( ) ;
2019-07-23 15:27:55 +00:00
* /
seek ( offset : Time , when? : Time ) : this {
const computedTime = this . toSeconds ( when ) ;
if ( this . _state . getValueAtTime ( computedTime ) === "started" ) {
2019-11-18 19:48:24 +00:00
const computedOffset = this . toSeconds ( offset ) ;
2019-07-23 15:27:55 +00:00
// if it's currently playing, stop it
this . _stop ( computedTime ) ;
// restart it at the given time
2019-11-18 19:48:24 +00:00
this . _start ( computedTime , computedOffset ) ;
2019-07-23 15:27:55 +00:00
}
return this ;
}
/ * *
* Set the loop start and end . Will only loop if loop is set to true .
2019-12-04 15:52:40 +00:00
* @param loopStart The loop start time
2019-07-23 15:27:55 +00:00
* @param loopEnd The loop end time
* @example
2020-06-08 00:14:48 +00:00
* const player = new Tone . Player ( "https://tonejs.github.io/audio/berklee/malevoices_aa2_F3.mp3" ) . toDestination ( ) ;
2019-10-25 20:54:33 +00:00
* // loop between the given points
2019-07-23 15:27:55 +00:00
* player . setLoopPoints ( 0.2 , 0.3 ) ;
* player . loop = true ;
2019-10-25 20:54:33 +00:00
* player . autostart = true ;
2019-07-23 15:27:55 +00:00
* /
setLoopPoints ( loopStart : Time , loopEnd : Time ) : this {
this . loopStart = loopStart ;
this . loopEnd = loopEnd ;
return this ;
}
/ * *
* If loop is true , the loop will start at this position .
* /
get loopStart ( ) : Time {
return this . _loopStart ;
}
set loopStart ( loopStart ) {
this . _loopStart = loopStart ;
2019-11-19 20:53:54 +00:00
if ( this . buffer . loaded ) {
assertRange ( this . toSeconds ( loopStart ) , 0 , this . buffer . duration ) ;
}
2019-07-23 15:27:55 +00:00
// get the current source
2021-12-20 16:59:20 +00:00
this . _activeSources . forEach ( ( source ) = > {
2019-07-23 15:27:55 +00:00
source . loopStart = loopStart ;
} ) ;
}
/ * *
* If loop is true , the loop will end at this position .
* /
get loopEnd ( ) : Time {
return this . _loopEnd ;
}
set loopEnd ( loopEnd ) {
this . _loopEnd = loopEnd ;
2019-11-19 20:53:54 +00:00
if ( this . buffer . loaded ) {
assertRange ( this . toSeconds ( loopEnd ) , 0 , this . buffer . duration ) ;
}
2019-07-23 15:27:55 +00:00
// get the current source
2021-12-20 16:59:20 +00:00
this . _activeSources . forEach ( ( source ) = > {
2019-07-23 15:27:55 +00:00
source . loopEnd = loopEnd ;
} ) ;
}
/ * *
* The audio buffer belonging to the player .
* /
get buffer ( ) : ToneAudioBuffer {
return this . _buffer ;
}
set buffer ( buffer ) {
this . _buffer . set ( buffer ) ;
}
/ * *
* If the buffer should loop once it ' s over .
2020-06-08 00:14:48 +00:00
* @example
* const player = new Tone . Player ( "https://tonejs.github.io/audio/drum-samples/breakbeat.mp3" ) . toDestination ( ) ;
* player . loop = true ;
* player . autostart = true ;
2019-07-23 15:27:55 +00:00
* /
get loop ( ) : boolean {
return this . _loop ;
}
set loop ( loop ) {
// if no change, do nothing
if ( this . _loop === loop ) {
return ;
}
this . _loop = loop ;
// set the loop of all of the sources
2021-12-20 16:59:20 +00:00
this . _activeSources . forEach ( ( source ) = > {
2019-07-23 15:27:55 +00:00
source . loop = loop ;
} ) ;
if ( loop ) {
// remove the next stopEvent
const stopEvent = this . _state . getNextState ( "stopped" , this . now ( ) ) ;
if ( stopEvent ) {
this . _state . cancel ( stopEvent . time ) ;
}
}
}
/ * *
2020-06-08 00:14:48 +00:00
* Normal speed is 1 . The pitch will change with the playback rate .
* @example
* const player = new Tone . Player ( "https://tonejs.github.io/audio/berklee/femalevoices_aa2_A5.mp3" ) . toDestination ( ) ;
* // play at 1/4 speed
* player . playbackRate = 0.25 ;
* // play as soon as the buffer is loaded
* player . autostart = true ;
2019-07-23 15:27:55 +00:00
* /
get playbackRate ( ) : Positive {
return this . _playbackRate ;
}
set playbackRate ( rate ) {
this . _playbackRate = rate ;
const now = this . now ( ) ;
// cancel the stop event since it's at a different time now
const stopEvent = this . _state . getNextState ( "stopped" , now ) ;
if ( stopEvent && stopEvent . implicitEnd ) {
this . _state . cancel ( stopEvent . time ) ;
2021-12-20 16:59:20 +00:00
this . _activeSources . forEach ( ( source ) = > source . cancelStop ( ) ) ;
2019-07-23 15:27:55 +00:00
}
// set all the sources
2021-12-20 16:59:20 +00:00
this . _activeSources . forEach ( ( source ) = > {
2019-07-23 15:27:55 +00:00
source . playbackRate . setValueAtTime ( rate , now ) ;
} ) ;
}
/ * *
2021-12-20 16:59:20 +00:00
* If the buffer should be reversed . Note that this sets the underlying [ [ ToneAudioBuffer . reverse ] ] , so
* if multiple players are pointing at the same ToneAudioBuffer , they will all be reversed .
2020-06-08 00:14:48 +00:00
* @example
* const player = new Tone . Player ( "https://tonejs.github.io/audio/berklee/chime_1.mp3" ) . toDestination ( ) ;
* player . autostart = true ;
* player . reverse = true ;
2019-07-23 15:27:55 +00:00
* /
get reverse ( ) : boolean {
return this . _buffer . reverse ;
}
set reverse ( rev ) {
this . _buffer . reverse = rev ;
}
/ * *
* If the buffer is loaded
* /
get loaded ( ) : boolean {
return this . _buffer . loaded ;
}
dispose ( ) : this {
super . dispose ( ) ;
// disconnect all of the players
2021-12-20 16:59:20 +00:00
this . _activeSources . forEach ( ( source ) = > source . dispose ( ) ) ;
2019-07-23 17:47:36 +00:00
this . _activeSources . clear ( ) ;
2019-07-23 15:27:55 +00:00
this . _buffer . dispose ( ) ;
return this ;
}
}