-
Notifications
You must be signed in to change notification settings - Fork 1k
Adding loop functionality to Sampler (#1310) #1350
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,9 +10,9 @@ | |
Note, | ||
Time, | ||
} from "../core/type/Units.js"; | ||
import { assert } from "../core/util/Debug.js"; | ||
import { assert, assertRange } from "../core/util/Debug.js"; | ||
import { timeRange } from "../core/util/Decorator.js"; | ||
import { optionsFromArguments } from "../core/util/Defaults.js"; | ||
import { defaultArg, optionsFromArguments } from "../core/util/Defaults.js"; | ||
import { noOp } from "../core/util/Interface.js"; | ||
import { isArray, isNote, isNumber } from "../core/util/TypeCheck.js"; | ||
import { Instrument, InstrumentOptions } from "../instrument/Instrument.js"; | ||
|
@@ -34,6 +34,9 @@ | |
baseUrl: string; | ||
curve: ToneBufferSourceCurve; | ||
urls: SamplesMap; | ||
loop: boolean; | ||
loopEnd: number; | ||
loopStart: number; | ||
} | ||
|
||
/** | ||
|
@@ -70,6 +73,26 @@ | |
*/ | ||
private _activeSources: Map<MidiNote, ToneBufferSource[]> = new Map(); | ||
|
||
/** | ||
* The list of all provided midi notes | ||
*/ | ||
private _providedMidiNotes: MidiNote[] = []; | ||
|
||
/** | ||
* if the buffer should loop once its over | ||
*/ | ||
private _loop: boolean; | ||
|
||
/** | ||
* if 'loop' is true, the loop will start at this position | ||
*/ | ||
private _loopStart: Time; | ||
|
||
/** | ||
* if 'loop' is true, the loop will end at this position | ||
*/ | ||
private _loopEnd: Time; | ||
|
||
/** | ||
* The envelope applied to the beginning of the sample. | ||
* @min 0 | ||
|
@@ -144,6 +167,9 @@ | |
this.attack = options.attack; | ||
this.release = options.release; | ||
this.curve = options.curve; | ||
this._loop = options.loop; | ||
this._loopStart = options.loopStart; | ||
this._loopEnd = options.loopEnd; | ||
|
||
// invoke the callback if it's already loaded | ||
if (this._buffers.loaded) { | ||
|
@@ -161,6 +187,9 @@ | |
onerror: noOp, | ||
release: 0.1, | ||
urls: {}, | ||
loop: false, | ||
loopEnd: 0, | ||
loopStart: 0, | ||
}); | ||
} | ||
|
||
|
@@ -197,6 +226,7 @@ | |
if (!Array.isArray(notes)) { | ||
notes = [notes]; | ||
} | ||
const offset = defaultArg(0, this._loopStart); | ||
notes.forEach((note) => { | ||
const midiFloat = ftomf( | ||
new FrequencyClass(this.context, note).toFrequency() | ||
|
@@ -210,16 +240,22 @@ | |
const playbackRate = intervalToFrequencyRatio( | ||
difference + remainder | ||
); | ||
let duration = this._loop | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: if duration isn't redefined, could be |
||
? undefined | ||
: buffer.duration / playbackRate; | ||
// play that note | ||
const source = new ToneBufferSource({ | ||
url: buffer, | ||
context: this.context, | ||
curve: this.curve, | ||
fadeIn: this.attack, | ||
fadeOut: this.release, | ||
loop: this._loop, | ||
loopStart: this._loopStart, | ||
loopEnd: this._loopEnd, | ||
playbackRate, | ||
}).connect(this.output); | ||
source.start(time, 0, buffer.duration / playbackRate, velocity); | ||
source.start(time, offset, duration, velocity); | ||
// add it to the active sources | ||
if (!isArray(this._activeSources.get(midi))) { | ||
this._activeSources.set(midi, []); | ||
|
@@ -357,6 +393,90 @@ | |
return this._buffers.loaded; | ||
} | ||
|
||
/** | ||
* Set the loop start and end. Will only loop if loop is set to true. | ||
* @param loopStart The loop start time | ||
* @param loopEnd The loop end time | ||
* @example | ||
* const sampler = new Tone.Sampler("https://tonejs.github.io/audio/berklee/guitar_chord4.mp3").toDestination(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: this const sampler = new Tone.Sampler({
urls: {
A1: "https://tonejs.github.io/audio/berklee/guitar_chord4.mp3",
},
}).toDestination();
// loop between the given points
sampler.setLoopPoints(0.2, 0.3);
sampler.loop = true; |
||
* // loop between the given points | ||
* sampler.setLoopPoints(0.2, 0.3); | ||
* sampler.loop = true; | ||
*/ | ||
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; | ||
this._providedMidiNotes.forEach((midiNote) => { | ||
const buffer = this._buffers.get(midiNote); | ||
if (buffer.loaded) { | ||
assertRange(this.toSeconds(loopStart), 0, buffer.duration); | ||
} | ||
}); | ||
// get the current sources | ||
this._activeSources.forEach((sourceList) => { | ||
sourceList.forEach((source) => { | ||
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; | ||
this._providedMidiNotes.forEach((midiNote) => { | ||
const buffer = this._buffers.get(midiNote); | ||
if (buffer.loaded) { | ||
assertRange(this.toSeconds(loopEnd), 0, buffer.duration); | ||
} | ||
}); | ||
// get the current sources | ||
this._activeSources.forEach((sourceList) => { | ||
sourceList.forEach((source) => { | ||
source.loopStart = loopEnd; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like you accidentally set loopStart instead of loopEnd here. |
||
}); | ||
}); | ||
} | ||
|
||
|
||
/** | ||
* If the buffers should loop once they are over. | ||
* @example | ||
* const sampler = new Tone.Sampler("https://tonejs.github.io/audio/berklee/femalevoice_aa_A4.mp3").toDestination(); | ||
* sampler.loop = true; | ||
*/ | ||
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 | ||
this._activeSources.forEach((sourceList) => { | ||
sourceList.forEach((source) => { | ||
source.loop = loop; | ||
}); | ||
}); | ||
} | ||
|
||
/** | ||
* Clean up | ||
*/ | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you have this reversed, should be
const offset = defaultArg(this._loopStart, 0);