From a6f8b57b2cd72e1b0fe492634f84f2be41f61f7b Mon Sep 17 00:00:00 2001 From: LoopBraker Date: Sat, 3 Jan 2026 07:39:53 -0500 Subject: [PATCH 1/4] feat(Sampler): add optional start/end parameters to triggerAttack Allows playing a portion of a sample without requiring loop mode. This enables smooth playback from arbitrary positions with the envelope's fadeIn applied to prevent clicking artifacts. Usage: sampler.triggerAttack("C4", time, velocity, startTime, endTime) --- Tone/instrument/Sampler.ts | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/Tone/instrument/Sampler.ts b/Tone/instrument/Sampler.ts index 5a625079f..4ae8786b3 100644 --- a/Tone/instrument/Sampler.ts +++ b/Tone/instrument/Sampler.ts @@ -216,17 +216,26 @@ export class Sampler extends Instrument { * @param notes The note to play, or an array of notes. * @param time When to play the note * @param velocity The velocity to play the sample back. + * @param start Optional start position within the sample (in seconds). + * Allows playing a portion of the sample without loop mode. + * @param end Optional end position within the sample (in seconds). + * If provided with start, plays only that portion of the sample. */ triggerAttack( notes: Frequency | Frequency[], time?: Time, - velocity: NormalRange = 1 + velocity: NormalRange = 1, + start?: Time, + end?: Time ): this { - this.log("triggerAttack", notes, time, velocity); + this.log("triggerAttack", notes, time, velocity, start, end); if (!Array.isArray(notes)) { notes = [notes]; } - const offset = defaultArg(this._loopStart, 0); + // Use custom start position if provided, otherwise fall back to loopStart or 0 + const offset = start !== undefined + ? this.toSeconds(start) + : defaultArg(this._loopStart, 0); notes.forEach((note) => { const midiFloat = ftomf( new FrequencyClass(this.context, note).toFrequency() @@ -240,9 +249,20 @@ export class Sampler extends Instrument { const playbackRate = intervalToFrequencyRatio( difference + remainder ); - const duration = this._loop - ? undefined - : buffer.duration / playbackRate; + // Calculate duration: custom range, loop mode, or full buffer + let duration: number | undefined; + if (start !== undefined && end !== undefined) { + // Custom range: calculate duration from start to end, adjusted for playback rate + const startSeconds = this.toSeconds(start); + const endSeconds = this.toSeconds(end); + duration = (endSeconds - startSeconds) / playbackRate; + } else if (this._loop) { + // Loop mode: let it loop indefinitely + duration = undefined; + } else { + // Default: play entire buffer + duration = buffer.duration / playbackRate; + } // play that note const source = new ToneBufferSource({ url: buffer, From f759b488b7e62a7ea319e04c814d704986b59cf8 Mon Sep 17 00:00:00 2001 From: LoopBraker Date: Sat, 3 Jan 2026 07:44:53 -0500 Subject: [PATCH 2/4] feat(Sampler): add reverse playback support Adds a `reverse` property to Sampler that enables reversed sample playback. When combined with start/end parameters in triggerAttack, allows playing portions of samples backwards while still benefiting from the envelope's fadeIn/fadeOut to prevent clicking artifacts. Usage: sampler.reverse = true; sampler.triggerAttack("A4", time, velocity, 0.5, 1.0); // plays 1.0s->0.5s reversed --- Tone/instrument/Sampler.ts | 85 +++++++++++++++++++++++++++++++------- 1 file changed, 70 insertions(+), 15 deletions(-) diff --git a/Tone/instrument/Sampler.ts b/Tone/instrument/Sampler.ts index 4ae8786b3..a5b264071 100644 --- a/Tone/instrument/Sampler.ts +++ b/Tone/instrument/Sampler.ts @@ -37,6 +37,7 @@ export interface SamplerOptions extends InstrumentOptions { loop: boolean; loopEnd: number; loopStart: number; + reverse: boolean; } /** @@ -93,6 +94,11 @@ export class Sampler extends Instrument { */ private _loopEnd: Time; + /** + * If the samples should be played in reverse + */ + private _reverse: boolean; + /** * The envelope applied to the beginning of the sample. * @min 0 @@ -170,6 +176,7 @@ export class Sampler extends Instrument { this._loop = options.loop; this._loopStart = options.loopStart; this._loopEnd = options.loopEnd; + this._reverse = options.reverse; // invoke the callback if it's already loaded if (this._buffers.loaded) { @@ -190,6 +197,7 @@ export class Sampler extends Instrument { loop: false, loopEnd: 0, loopStart: 0, + reverse: false, }); } @@ -232,10 +240,6 @@ export class Sampler extends Instrument { if (!Array.isArray(notes)) { notes = [notes]; } - // Use custom start position if provided, otherwise fall back to loopStart or 0 - const offset = start !== undefined - ? this.toSeconds(start) - : defaultArg(this._loopStart, 0); notes.forEach((note) => { const midiFloat = ftomf( new FrequencyClass(this.context, note).toFrequency() @@ -249,20 +253,50 @@ export class Sampler extends Instrument { const playbackRate = intervalToFrequencyRatio( difference + remainder ); - // Calculate duration: custom range, loop mode, or full buffer + + // Calculate offset and duration based on reverse mode + let offset: number; let duration: number | undefined; - if (start !== undefined && end !== undefined) { - // Custom range: calculate duration from start to end, adjusted for playback rate - const startSeconds = this.toSeconds(start); - const endSeconds = this.toSeconds(end); - duration = (endSeconds - startSeconds) / playbackRate; - } else if (this._loop) { - // Loop mode: let it loop indefinitely - duration = undefined; + + if (this._reverse) { + // Reverse the buffer if not already reversed + if (!buffer.reverse) { + buffer.reverse = true; + } + // When reversed, start/end refer to the original (non-reversed) buffer positions + // We need to flip them: original "end" becomes the offset in reversed buffer + if (start !== undefined && end !== undefined) { + const startSeconds = this.toSeconds(start); + const endSeconds = this.toSeconds(end); + // In reversed buffer: offset = buffer.duration - end (where we want to start from) + offset = buffer.duration - endSeconds; + duration = (endSeconds - startSeconds) / playbackRate; + } else { + // No custom range, play from beginning of reversed buffer + offset = this.toSeconds(defaultArg(this._loopStart, 0)); + duration = this._loop ? undefined : buffer.duration / playbackRate; + } } else { - // Default: play entire buffer - duration = buffer.duration / playbackRate; + // Ensure buffer is not reversed + if (buffer.reverse) { + buffer.reverse = false; + } + // Use custom start position if provided, otherwise fall back to loopStart or 0 + offset = start !== undefined + ? this.toSeconds(start) + : this.toSeconds(defaultArg(this._loopStart, 0)); + // Calculate duration: custom range, loop mode, or full buffer + if (start !== undefined && end !== undefined) { + const startSeconds = this.toSeconds(start); + const endSeconds = this.toSeconds(end); + duration = (endSeconds - startSeconds) / playbackRate; + } else if (this._loop) { + duration = undefined; + } else { + duration = buffer.duration / playbackRate; + } } + // play that note const source = new ToneBufferSource({ url: buffer, @@ -504,6 +538,27 @@ export class Sampler extends Instrument { }); }); } + + /** + * If the samples should be played in reverse. + * When reverse is true and start/end are provided to triggerAttack, + * the sample will play backwards from end to start. + * @example + * const sampler = new Tone.Sampler({ + * urls: { + * A4: "https://tonejs.github.io/audio/berklee/femalevoice_aa_A4.mp3", + * }, + * }).toDestination(); + * sampler.reverse = true; + * // Play in reverse from 1.0s to 0.5s (in original time) + * sampler.triggerAttack("A4", Tone.now(), 1, 0.5, 1.0); + */ + get reverse(): boolean { + return this._reverse; + } + set reverse(rev: boolean) { + this._reverse = rev; + } /** * Clean up From b0a50b8df991be3dd4bcbe40156e217fd284a12d Mon Sep 17 00:00:00 2001 From: LoopBraker Date: Sat, 3 Jan 2026 08:57:14 -0500 Subject: [PATCH 3/4] Improve example --- examples/js/ExampleList.json | 3 +- examples/samplerRange.html | 932 +++++++++++++++++++++++++++++++++++ package-lock.json | 233 +++++---- 3 files changed, 1045 insertions(+), 123 deletions(-) create mode 100644 examples/samplerRange.html diff --git a/examples/js/ExampleList.json b/examples/js/ExampleList.json index 823c66147..9593abe4c 100644 --- a/examples/js/ExampleList.json +++ b/examples/js/ExampleList.json @@ -16,7 +16,8 @@ "FatOscillator" : "jump", "MetalSynth" : "bembe", "Granular Synthesis" : "grainPlayer", - "Sampler" : "sampler" + "Sampler" : "sampler", + "Sampler Range" : "samplerRange" }, "Effects" : { "LFO Effects" : "lfoEffects", diff --git a/examples/samplerRange.html b/examples/samplerRange.html new file mode 100644 index 000000000..21eb93b00 --- /dev/null +++ b/examples/samplerRange.html @@ -0,0 +1,932 @@ + + + + + Sampler Range Playback + + + + + + + + + + + + + +
+ This example demonstrates the extended + Tone.Sampler + with support for start/end range and reverse playback. + Upload a .wav or .mp3 file, adjust the start and end positions, + toggle reverse mode, and play portions of your sample with envelope + smoothing to prevent clicks. +
+ +
+
+ +
+ + +
+ + +
+ + + + + 1.0x + + + +
+ + +
+
+ + +
+
+
+
+
+ + +
+ + + + 0.00s +
+ + +
+ + + + 1.00s +
+ + +
+ + + 0.01s +
+ + +
+ + + 0.10s +
+ + +
+ + +
+ + +
+ + + + + +
+ + +
Upload a sample to begin...
+
+
+
+ + + + diff --git a/package-lock.json b/package-lock.json index 99de77df5..0c67391dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1387,9 +1387,9 @@ "license": "Python-2.0" }, "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -1423,19 +1423,32 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", - "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.14.0", + "@eslint/core": "^0.15.2", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@gerrit0/mini-shiki": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.12.0.tgz", @@ -3192,9 +3205,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4301,24 +4314,24 @@ } }, "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", "dev": true, "license": "MIT", "dependencies": { - "bytes": "3.1.2", + "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", "type-is": "~1.6.18", - "unpipe": "1.0.0" + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8", @@ -4336,20 +4349,24 @@ } }, "node_modules/body-parser/node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "dev": true, "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/body-parser/node_modules/ms": { @@ -4359,26 +4376,10 @@ "dev": true, "license": "MIT" }, - "node_modules/body-parser/node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/body-parser/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "dev": true, "license": "MIT", "engines": { @@ -4404,9 +4405,9 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -5300,9 +5301,9 @@ } }, "node_modules/cosmiconfig/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -6642,40 +6643,40 @@ "license": "ISC" }, "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "dev": true, "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", + "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.13.0", + "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", + "send": "~0.19.0", + "serve-static": "~1.16.2", "setprototypeof": "1.2.0", - "statuses": "2.0.1", + "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" @@ -6732,22 +6733,6 @@ "dev": true, "license": "MIT" }, - "node_modules/express/node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/express/node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -7337,9 +7322,9 @@ "license": "BSD-2-Clause" }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8688,9 +8673,9 @@ } }, "node_modules/koa": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/koa/-/koa-2.16.1.tgz", - "integrity": "sha512-umfX9d3iuSxTQP4pnzLOz0HKnPg0FaUUIKcye2lOiz3KPu1Y3M3xlz76dISdFPQs37P9eJz1wUpcTS6KDPn9fA==", + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.16.3.tgz", + "integrity": "sha512-zPPuIt+ku1iCpFBRwseMcPYQ1cJL8l60rSmKeOuGfOXyE6YnTBmf2aEFNL2HQGrD0cPcLO/t+v9RTgC+fwEh/g==", "dev": true, "license": "MIT", "dependencies": { @@ -9397,9 +9382,9 @@ "license": "Python-2.0" }, "node_modules/mocha/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -9417,9 +9402,9 @@ } }, "node_modules/mocha/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -10502,9 +10487,9 @@ } }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -10559,42 +10544,46 @@ } }, "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "dev": true, "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8" } }, "node_modules/raw-body/node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "dev": true, "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/raw-body/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "dev": true, "license": "MIT", "engines": { @@ -12454,9 +12443,9 @@ } }, "node_modules/typedoc/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { From e38bc2c03077e75c499fd8badc68e56bf440c791 Mon Sep 17 00:00:00 2001 From: LoopBraker Date: Sat, 3 Jan 2026 16:11:12 -0500 Subject: [PATCH 4/4] Attack fades in at the start position, Release fades out completing exactly at the end position --- Tone/instrument/Sampler.ts | 11 +- examples/js/ExampleList.json | 3 +- examples/samplerRangeModern.html | 786 +++++++++++++++++++++++++++++++ 3 files changed, 796 insertions(+), 4 deletions(-) create mode 100644 examples/samplerRangeModern.html diff --git a/Tone/instrument/Sampler.ts b/Tone/instrument/Sampler.ts index a5b264071..de217950a 100644 --- a/Tone/instrument/Sampler.ts +++ b/Tone/instrument/Sampler.ts @@ -258,6 +258,9 @@ export class Sampler extends Instrument { let offset: number; let duration: number | undefined; + // Get the release time in seconds for duration adjustment + const releaseSeconds = this.toSeconds(this.release); + if (this._reverse) { // Reverse the buffer if not already reversed if (!buffer.reverse) { @@ -270,7 +273,9 @@ export class Sampler extends Instrument { const endSeconds = this.toSeconds(end); // In reversed buffer: offset = buffer.duration - end (where we want to start from) offset = buffer.duration - endSeconds; - duration = (endSeconds - startSeconds) / playbackRate; + // Subtract release time so fadeout completes exactly at 'start' position + const rangeDuration = (endSeconds - startSeconds) / playbackRate; + duration = Math.max(0, rangeDuration - releaseSeconds); } else { // No custom range, play from beginning of reversed buffer offset = this.toSeconds(defaultArg(this._loopStart, 0)); @@ -289,7 +294,9 @@ export class Sampler extends Instrument { if (start !== undefined && end !== undefined) { const startSeconds = this.toSeconds(start); const endSeconds = this.toSeconds(end); - duration = (endSeconds - startSeconds) / playbackRate; + // Subtract release time so fadeout completes exactly at 'end' position + const rangeDuration = (endSeconds - startSeconds) / playbackRate; + duration = Math.max(0, rangeDuration - releaseSeconds); } else if (this._loop) { duration = undefined; } else { diff --git a/examples/js/ExampleList.json b/examples/js/ExampleList.json index 9593abe4c..823c66147 100644 --- a/examples/js/ExampleList.json +++ b/examples/js/ExampleList.json @@ -16,8 +16,7 @@ "FatOscillator" : "jump", "MetalSynth" : "bembe", "Granular Synthesis" : "grainPlayer", - "Sampler" : "sampler", - "Sampler Range" : "samplerRange" + "Sampler" : "sampler" }, "Effects" : { "LFO Effects" : "lfoEffects", diff --git a/examples/samplerRangeModern.html b/examples/samplerRangeModern.html new file mode 100644 index 000000000..21fad54b3 --- /dev/null +++ b/examples/samplerRangeModern.html @@ -0,0 +1,786 @@ + + + + + Sampler Range Playback + + + + + + + + + + +
+

Sampler Range & Loop

+ + +
+ + + + + 1.0x +
+ + +
+ + +
+
+ + +
+
+
+
+ + +
+ +
+ + +
+ +
+ + + 0.00s +
+ +
+ + + 1.00s +
+ +
+ + + + +
+
+ + +
+ + +
+ + + +
+ +
Upload a sample...
+
+ + + + \ No newline at end of file