audio-slot
Advanced tools
Comparing version 4.9.2 to 5.0.0
@@ -26,5 +26,2 @@ var Observ = require('observ') | ||
var refreshingConnections = false | ||
var extraConnections = [] | ||
input.connect(pre) | ||
@@ -145,3 +142,8 @@ pre.connect(toProcessors) | ||
} | ||
}) | ||
} | ||
obs.cancelFrom = function (at) { | ||
obs.sources.forEach(function (source) { | ||
source.cancelFrom && source.cancelFrom(at) | ||
}) | ||
@@ -148,0 +150,0 @@ } |
@@ -100,5 +100,6 @@ var Observ = require('observ') | ||
lastBuffer = buffer() | ||
obs.set(true) | ||
if (obs()) { | ||
obs.set(true) | ||
} | ||
} | ||
} | ||
@@ -105,0 +106,0 @@ } |
{ | ||
"name": "audio-slot", | ||
"version": "4.9.2", | ||
"version": "5.0.0", | ||
"description": "Web Audio API FRP wrapper for creating, routing, and triggering AudioNodes.", | ||
@@ -16,3 +16,3 @@ "main": "index.js", | ||
"array-union": "^1.0.1", | ||
"audio-slot-param": "~1.1.1", | ||
"audio-slot-param": "~2.2.0", | ||
"bitcrusher": "^0.3.0", | ||
@@ -22,5 +22,7 @@ "deep-equal": "~0.2.1", | ||
"geval": "^2.1.1", | ||
"make-distortion-curve": "^1.0.0", | ||
"noise-buffer": "^2.0.0", | ||
"observ": "^0.2.0", | ||
"observ-default": "^1.0.0", | ||
"observ-node-array": "^1.10.0", | ||
"observ-node-array": "^1.11.1", | ||
"observ-struct": "^6.0.0", | ||
@@ -27,0 +29,0 @@ "setimmediate2": "^2.0.1", |
@@ -13,6 +13,6 @@ var ObservStruct = require('observ-struct') | ||
var obs = ObservStruct({ | ||
attack: Property(0), | ||
decay: Property(0), | ||
sustain: Property(1), | ||
release: Property(0), | ||
attack: Param(context, 0), | ||
decay: Param(context, 0), | ||
release: Param(context, 0), | ||
sustain: Param(context, 1), | ||
value: Param(context, 1) | ||
@@ -33,3 +33,3 @@ }) | ||
{ param: obs.value }, | ||
{ param: eventSource, transform: multiply } | ||
{ param: eventSource, transform: multiply, watchingYou: true } | ||
]) | ||
@@ -49,6 +49,5 @@ | ||
broadcast({ | ||
fromValue: 0, | ||
value: 1, | ||
at: at, | ||
duration: obs.attack(), | ||
duration: obs.attack.getValueAt(at), | ||
mode: 'log' | ||
@@ -62,5 +61,5 @@ }) | ||
broadcast({ | ||
value: obs.sustain(), | ||
value: obs.sustain.getValueAt(peakTime), | ||
at: peakTime, | ||
duration: obs.decay(), | ||
duration: obs.decay.getValueAt(peakTime), | ||
mode: 'log' | ||
@@ -77,3 +76,3 @@ }) | ||
value: 0, at: at, | ||
duration: obs.release(), | ||
duration: obs.release.getValueAt(at), | ||
mode: 'log' | ||
@@ -88,2 +87,7 @@ }) | ||
obs.cancelFrom = function (at) { | ||
at = Math.max(at, context.audio.currentTime) | ||
broadcast({ mode: 'cancel', at: at }) | ||
} | ||
obs.getReleaseDuration = function () { | ||
@@ -90,0 +94,0 @@ return obs.release() |
@@ -33,3 +33,3 @@ var Observ = require('observ') | ||
curve: Param(context, 0.5), | ||
curve: Param(context, 1), | ||
skew: Param(context, 0) | ||
@@ -58,3 +58,3 @@ }) | ||
var transform = Transform(context, [ | ||
{ param: eventSource }, | ||
{ param: eventSource, transform: offsetForOperation }, | ||
{ param: obs.amp, transform: multiply }, | ||
@@ -172,10 +172,8 @@ { param: obs.value, transform: operation } | ||
function step (start, duration) { | ||
var skew = clamp((obs.skew.getValueAt(start) + 1) / 2, 0, 0.999999999) | ||
var skew = clamp((obs.skew.getValueAt(start) + 1), 0, 1.9999999999) | ||
var curve = clamp(obs.curve.getValueAt(start), 0, 1) | ||
var stepDuration = duration / 4 | ||
var up = stepDuration * skew * curve | ||
var down = stepDuration * (1 - skew) * curve | ||
var mid = start + (duration * skew) | ||
var end = start + duration | ||
var pause = (stepDuration - curve * stepDuration) * 2 | ||
var down = stepDuration * (2 - skew) * curve | ||
@@ -189,3 +187,3 @@ broadcast({ | ||
broadcast({ | ||
at: mid - down, | ||
at: start + up + pause, | ||
value: 0, | ||
@@ -196,13 +194,23 @@ duration: down | ||
broadcast({ | ||
at: mid + up, | ||
at: start + up + pause + down, | ||
value: -1, | ||
duration: up | ||
duration: down | ||
}) | ||
broadcast({ | ||
at: end - down, | ||
at: start + up + pause + down + down + pause, | ||
value: 0, | ||
duration: down | ||
duration: up | ||
}) | ||
} | ||
function offsetForOperation (_, value) { | ||
var mode = obs.mode() | ||
if (mode === 'add') { | ||
return value | ||
} else if (mode === 'subtract') { | ||
return value | ||
} else { | ||
return (value + 1) / 2 | ||
} | ||
} | ||
@@ -209,0 +217,0 @@ |
@@ -6,3 +6,3 @@ var ObservStruct = require('observ-struct') | ||
function ProcessorNode (context, input, output, params) { | ||
function ProcessorNode (context, input, output, params, releases) { | ||
var obs = ObservStruct(params) | ||
@@ -28,3 +28,18 @@ | ||
obs.cancelFrom = function (at) { | ||
Param.cancelFrom(obs, at) | ||
} | ||
obs.destroy = function () { | ||
while (releases && releases.length) { | ||
releases.pop()() | ||
} | ||
Object.keys(obs).forEach(function (key) { | ||
if (obs[key] && typeof obs[key].destroy === 'function') { | ||
obs[key].destroy() | ||
} | ||
}) | ||
} | ||
return obs | ||
} |
@@ -33,2 +33,4 @@ var Processor = require('../processor.js') | ||
var releases = [] | ||
var obs = Processor(context, input, output, { | ||
@@ -44,3 +46,3 @@ time: Param(context, 0.25), | ||
dry: Param(context, 1) | ||
}) | ||
}, releases) | ||
@@ -52,2 +54,5 @@ var rateMultiplier = Transform(context, [ | ||
// release context.tempo | ||
releases.push(rateMultiplier.destroy) | ||
var time = Transform(context, [ | ||
@@ -70,7 +75,2 @@ { param: obs.time }, | ||
obs.destroy = function () { | ||
// release context.tempo | ||
rateMultiplier.destroy() | ||
} | ||
return obs | ||
@@ -77,0 +77,0 @@ } |
@@ -25,3 +25,6 @@ var Processor = require('../processor.js') | ||
ratio: Param(context, 1) | ||
}) | ||
}, [ | ||
to.disconnect.bind(to), | ||
from.disconnect.bind(from) | ||
]) | ||
@@ -38,7 +41,2 @@ watch(obs.mode, function (value) { | ||
obs.destroy = function () { | ||
to.disconnect() | ||
from.disconnect() | ||
} | ||
Apply(context, to.gain, obs.ratio) | ||
@@ -45,0 +43,0 @@ Apply(context, from.gain, obs.ratio) |
@@ -23,2 +23,3 @@ var Processor = require('../processor.js') | ||
var wet = context.audio.createGain() | ||
var releases = [] | ||
@@ -52,3 +53,3 @@ // feedback loop | ||
dry: Param(context, 1) | ||
}) | ||
}, releases) | ||
@@ -60,2 +61,5 @@ var rateMultiplier = Transform(context, [ | ||
// release context.tempo | ||
releases.push(rateMultiplier.destroy) | ||
var time = Transform(context, [ | ||
@@ -79,7 +83,2 @@ { param: obs.time }, | ||
obs.destroy = function () { | ||
// release context.tempo | ||
rateMultiplier.destroy() | ||
} | ||
return obs | ||
@@ -86,0 +85,0 @@ } |
@@ -42,3 +42,3 @@ var Processor = require('../processor.js') | ||
dry: Param(context, 1) | ||
}) | ||
}, [cancel]) | ||
@@ -57,4 +57,6 @@ obs.time(refreshImpulse) | ||
obs.destroy = function () { | ||
// release context.tempo | ||
return obs | ||
// scoped | ||
function cancel () { | ||
if (building) { | ||
@@ -65,13 +67,6 @@ buildImpulse.cancel(building) | ||
return obs | ||
// scoped | ||
function refreshImpulse () { | ||
var rate = context.audio.sampleRate | ||
var length = Math.max(rate * obs.time(), 1) | ||
if (building) { | ||
buildImpulse.cancel(building) | ||
} | ||
cancel() | ||
building = buildImpulse(length, obs.decay(), obs.reverse(), function (channels) { | ||
@@ -78,0 +73,0 @@ var impulse = context.audio.createBuffer(2, length, rate) |
@@ -10,3 +10,3 @@ var Observ = require('observ') | ||
function RoutableSlot (context, properties, input, output) { | ||
function RoutableSlot (context, properties, input, output, releases) { | ||
var audioContext = context.audio | ||
@@ -56,2 +56,7 @@ | ||
obs.destroy = function () { | ||
Object.keys(obs).forEach(function (key) { | ||
if (obs[key] && typeof obs[key].destroy === 'function') { | ||
obs[key].destroy() | ||
} | ||
}) | ||
removeSlotWatcher && removeSlotWatcher() | ||
@@ -58,0 +63,0 @@ removeSlotWatcher = null |
@@ -1,12 +0,12 @@ | ||
var ObservStruct = require('observ-struct') | ||
var Node = require('observ-node-array/single') | ||
var ResolvedValue = require('observ-node-array/resolved-value') | ||
var Param = require('audio-slot-param') | ||
var Property = require('observ-default') | ||
var Param = require('audio-slot-param') | ||
var Transform = require('audio-slot-param/transform') | ||
var Apply = require('audio-slot-param/apply') | ||
var ResolvedValue = require('observ-node-array/resolved-value') | ||
var Triggerable = require('../triggerable') | ||
var ScheduleList = require('../lib/schedule-list') | ||
var ScheduleEvent = require('../lib/schedule-event') | ||
var SyncProperty = require('../lib/granular-sync') | ||
var applyScheduler = require('../lib/apply-scheduler') | ||
@@ -17,3 +17,5 @@ module.exports = GranularNode | ||
var output = context.audio.createGain() | ||
var releaseSchedule = applyScheduler(context, handleSchedule) | ||
var amp = context.audio.createGain() | ||
amp.gain.value = 0 | ||
amp.connect(output) | ||
@@ -26,3 +28,3 @@ var offset = Property([0, 1]) | ||
var obs = ObservStruct({ | ||
var obs = Triggerable(context, { | ||
mode: Property('loop'), | ||
@@ -44,6 +46,5 @@ | ||
amp: Param(context, 1) | ||
}) | ||
}, trigger) | ||
Apply(context, output.gain, obs.amp) | ||
obs.resolvedBuffer = resolvedBuffer | ||
obs.context = context | ||
@@ -57,125 +58,115 @@ | ||
obs.resolvedBuffer = resolvedBuffer | ||
Apply(context, amp.gain, obs.amp) | ||
var active = [] | ||
var scheduledTo = 0 | ||
var lastBeatDuration = 1 | ||
obs.connect = output.connect.bind(output) | ||
obs.disconnect = output.disconnect.bind(output) | ||
obs.choke = function (at) { | ||
at = at || context.audio.currentTime | ||
var event = eventAt(at - 0.02) | ||
if (event) { | ||
var stopAt = at + (0.02 * 6) | ||
event.output.gain.setTargetAtTime(0, at, 0.02) | ||
event.end = stopAt | ||
} | ||
return obs | ||
// scoped | ||
function trigger (at) { | ||
return new GranularSample(obs, amp, playbackRate, at) | ||
} | ||
} | ||
obs.triggerOn = function (at) { | ||
obs.choke(at) | ||
function noteOffsetToRate (baseRate, value) { | ||
return baseRate * Math.pow(2, value / 12) | ||
} | ||
var amp = context.audio.createGain() | ||
amp.connect(output) | ||
function centsToRate (baseRate, value) { | ||
return baseRate * Math.pow(2, value / 1200) | ||
} | ||
var stopAt = null | ||
var event = { | ||
start: at, | ||
end: null, | ||
nextTime: at, | ||
nextOffset: 0, | ||
output: amp | ||
} | ||
// internal class | ||
if (obs.mode() === 'oneshot') { | ||
event.oneshot = true | ||
var duration = obs.sync() ? obs.duration() * lastBeatDuration : obs.duration() | ||
stopAt = at + duration | ||
Param.triggerOff(obs, stopAt) | ||
truncate(stopAt) | ||
event.end = stopAt | ||
} | ||
function GranularSample (obs, output, playbackRate, from) { | ||
var clock = obs.context.scheduler | ||
var nextTime = clock.getNextScheduleTime() | ||
var schedule = { | ||
time: from, | ||
duration: nextTime - from, | ||
beatDuration: clock.getBeatDuration() | ||
} | ||
at = at || context.audio.currentTime | ||
truncate(at) | ||
var length = obs.duration() | ||
if (obs.sync()) { | ||
length = length * schedule.beatDuration | ||
} | ||
Param.triggerOn(obs, at) | ||
this.context = obs.context | ||
this.obs = obs | ||
this.from = from | ||
this.to = NaN | ||
this.nextTime = from | ||
this.nextOffset = 0 | ||
this.choker = obs.context.audio.createGain() | ||
this.choked = false | ||
this.oneshot = obs.mode() === 'oneshot' | ||
this.events = ScheduleList() | ||
this.releases = [this.events.destroy] | ||
this.playbackRate = playbackRate | ||
active.push(event) | ||
if (this.oneshot) { | ||
this.to = from + length | ||
} | ||
if (at < scheduledTo) { | ||
scheduleEvent(event, at, scheduledTo, lastBeatDuration) | ||
} | ||
this.choker.connect(output) | ||
return stopAt | ||
if (handleSchedule.call(this, schedule)) { | ||
this.releases.push(clock.onSchedule(handleSchedule.bind(this))) | ||
} | ||
} | ||
obs.triggerOff = function (at) { | ||
at = at || context.audio.currentTime | ||
var event = eventAt(at) | ||
if (event && !event.oneshot) { | ||
var stopAt = obs.getReleaseDuration() + at | ||
Param.triggerOff(obs, stopAt) | ||
truncate(stopAt) | ||
event.end = stopAt | ||
} | ||
GranularSample.prototype.choke = function (at) { | ||
if (!this.to || at < this.to) { | ||
this.choker.gain.cancelScheduledValues(this.choker.context.currentTime) | ||
this.choker.gain.setTargetAtTime(0, at, 0.02) | ||
this.choked = true | ||
this.to = at + 0.1 | ||
} | ||
} | ||
obs.destroy = function () { | ||
// release context.noteOffset | ||
playbackRate.destroy() | ||
releaseSchedule && releaseSchedule() | ||
releaseSchedule = null | ||
GranularSample.prototype.cancelChoke = function (at) { | ||
if (this.choked && this.stopAt) { | ||
this.choker.gain.cancelScheduledValues(this.to - 0.1) | ||
this.stop(this.stopAt) | ||
} | ||
} | ||
obs.getReleaseDuration = Param.getReleaseDuration.bind(this, obs) | ||
obs.connect = output.connect.bind(output) | ||
obs.disconnect = output.disconnect.bind(output) | ||
GranularSample.prototype.stop = function (at) { | ||
at = at || this.choker.context.currentTime | ||
this.events.truncateTo(this.context.audio.currentTime) | ||
this.choker.gain.cancelScheduledValues(this.choker.context.currentTime) | ||
this.choker.gain.setValueAtTime(0, at) | ||
this.choked = false | ||
this.stopAt = at | ||
this.to = at | ||
} | ||
return obs | ||
function handleSchedule (schedule) { | ||
var obs = this.obs | ||
var endTime = schedule.time + schedule.duration | ||
// scoped | ||
this.events.truncateTo(this.context.audio.currentTime) | ||
if (endTime >= this.from && (!this.to || schedule.time < this.to)) { | ||
var length = obs.duration() | ||
var rate = obs.rate() | ||
function handleSchedule (schedule) { | ||
var from = schedule.time | ||
var to = schedule.time + schedule.duration | ||
for (var i = active.length - 1;i >= 0;i--) { | ||
var event = active[i] | ||
// clean up old events | ||
if (event.end && event.end < context.audio.currentTime) { | ||
event.output.disconnect() | ||
active.splice(i, 1) | ||
continue | ||
} | ||
scheduleEvent(event, from, to, schedule.beatDuration) | ||
if (obs.sync()) { | ||
length = length * schedule.beatDuration | ||
rate = rate / schedule.beatDuration | ||
} | ||
lastBeatDuration = schedule.beatDuration | ||
scheduledTo = to | ||
} | ||
var slices = Math.max(1, rate) * length | ||
var duration = length / slices | ||
function scheduleEvent (event, from, to, beatDuration) { | ||
if (event.start <= from && (!event.end || event.end > to)) { | ||
var length = obs.duration() | ||
var rate = obs.rate() | ||
if (obs.sync()) { | ||
length = length * beatDuration | ||
rate = rate / beatDuration | ||
while (this.nextTime < endTime) { | ||
var event = play.call(this, this.nextTime, this.nextOffset, duration) | ||
if (event) { | ||
this.events.push(event) | ||
} | ||
var slices = Math.max(1, rate) * length | ||
var duration = length / slices | ||
while (event.nextTime < to) { | ||
play(event.output, event.nextTime, event.nextOffset, duration) | ||
event.nextTime += duration | ||
event.nextOffset += 1 / slices | ||
if (obs.mode() !== 'oneshot') { | ||
event.nextOffset = event.nextOffset % 1 | ||
} | ||
this.nextTime += duration | ||
this.nextOffset += 1 / slices | ||
if (obs.mode() !== 'oneshot') { | ||
this.nextOffset = this.nextOffset % 1 | ||
} | ||
@@ -185,71 +176,57 @@ } | ||
function play (output, at, startOffset, grainDuration) { | ||
var event = eventAt(at) | ||
if (!this.to || this.to > endTime) { | ||
return true | ||
} | ||
} | ||
var buffer = obs.resolvedBuffer() | ||
if (buffer instanceof window.AudioBuffer) { | ||
var source = context.audio.createBufferSource() | ||
source.buffer = buffer | ||
function play (at, startOffset, grainDuration) { | ||
var obs = this.obs | ||
var context = this.context | ||
var buffer = obs.resolvedBuffer() | ||
if (buffer instanceof window.AudioBuffer && isFinite(startOffset) && grainDuration) { | ||
var source = context.audio.createBufferSource() | ||
source.buffer = buffer | ||
var offset = obs.offset() | ||
var start = offset[0] * source.buffer.duration | ||
var end = offset[1] * source.buffer.duration | ||
var duration = end - start | ||
var offset = obs.offset() | ||
var start = offset[0] * source.buffer.duration | ||
var end = offset[1] * source.buffer.duration | ||
var duration = end - start | ||
var release = grainDuration * obs.release() | ||
var attack = grainDuration * obs.attack() | ||
var release = grainDuration * obs.release() | ||
var attack = grainDuration * obs.attack() | ||
// make sure it doesn't exceed the stop time | ||
var maxTime = (event && event.end || Infinity) - release | ||
var releaseAt = Math.min(at + grainDuration * obs.hold(), maxTime) | ||
// make sure it doesn't exceed the stop time | ||
var maxTime = (this.to || Infinity) - release | ||
var releaseAt = Math.min(at + grainDuration * obs.hold(), maxTime) | ||
source.playbackRate.value = playbackRate.getValueAt(at) | ||
source.playbackRate.value = this.playbackRate.getValueAt(at) | ||
if (obs.mode() !== 'oneshot' && releaseAt + release > startOffset * duration) { | ||
source.loop = true | ||
source.loopStart = start | ||
source.loopEnd = end | ||
} | ||
if (obs.mode() !== 'oneshot' && releaseAt + release > startOffset * duration) { | ||
source.loop = true | ||
source.loopStart = start | ||
source.loopEnd = end | ||
} | ||
source.start(at, startOffset * duration + start) | ||
source.stop(releaseAt + release * 2) | ||
source.start(at, startOffset * duration + start) | ||
source.stop(releaseAt + release * 2) | ||
var envelope = context.audio.createGain() | ||
envelope.gain.value = 0 | ||
source.connect(envelope) | ||
var envelope = context.audio.createGain() | ||
envelope.gain.value = 0 | ||
source.connect(envelope) | ||
// envelope | ||
if (attack) { | ||
envelope.gain.setTargetAtTime(1, at, attack / 4) | ||
} | ||
envelope.gain.setTargetAtTime(0, releaseAt, release / 4) | ||
envelope.connect(output) | ||
// envelope | ||
if (attack) { | ||
envelope.gain.setTargetAtTime(1, at, attack / 4) | ||
} | ||
} | ||
envelope.gain.setTargetAtTime(0, releaseAt, release / 4) | ||
envelope.connect(this.choker) | ||
function truncate (at) { | ||
for (var i = active.length - 1;i >= 0;i--) { | ||
if (active[i].start >= at) { | ||
active.splice(i, 1) | ||
} else if (active[i].end && active[i].end > at) { | ||
active[i].end = at | ||
} | ||
} | ||
} | ||
var event = new ScheduleEvent(at, source, envelope, [ | ||
Apply(context, source.playbackRate, this.playbackRate) | ||
]) | ||
function eventAt (time) { | ||
for (var i = 0;i < active.length;i++) { | ||
if (active[i].start <= time && (!active[i].end || active[i].end > time)) { | ||
return active[i] | ||
} | ||
} | ||
event.to = releaseAt + release | ||
return event | ||
} | ||
} | ||
function noteOffsetToRate (baseRate, value) { | ||
return baseRate * Math.pow(2, value / 12) | ||
} | ||
function centsToRate (baseRate, value) { | ||
return baseRate * Math.pow(2, value / 1200) | ||
} |
@@ -1,2 +0,2 @@ | ||
var computed = require('observ/computed') | ||
var computed = require('../lib/computed-next-tick') | ||
var ObservStruct = require('observ-struct') | ||
@@ -7,2 +7,4 @@ var Property = require('observ-default') | ||
var Apply = require('audio-slot-param/apply') | ||
var Triggerable = require('../triggerable') | ||
var ScheduleEvent = require('../lib/schedule-event') | ||
@@ -13,79 +15,46 @@ module.exports = NoiseNode | ||
var output = context.audio.createGain() | ||
var amp = context.audio.createGain() | ||
amp.gain.value = 0 | ||
amp.connect(output) | ||
var obs = ObservStruct({ | ||
var obs = Triggerable(context, { | ||
type: Property('white'), | ||
stereo: Property(false), | ||
amp: Param(context, 0.4) | ||
}) | ||
}, trigger) | ||
obs.context = context | ||
obs.resolvedBuffer = computed([obs.type, obs.stereo], function (type, stereo) { | ||
if (type === 'pink') { | ||
return generatePinkNoise(context.audio, 4096 * 2, stereo ? 2 : 1) | ||
return generatePinkNoise(context.audio, 4096 * 4, stereo ? 2 : 1) | ||
} else { | ||
return generateWhiteNoise(context.audio, 4096 * 2, stereo ? 2 : 1) | ||
return generateWhiteNoise(context.audio, 4096 * 4, stereo ? 2 : 1) | ||
} | ||
}) | ||
var player = null | ||
var choker = null | ||
var amp = null | ||
var releaseAmp = null | ||
var playTo = false | ||
obs.context = context | ||
obs.getReleaseDuration = Param.getReleaseDuration.bind(this, obs) | ||
Apply(context, amp.gain, obs.amp) | ||
obs.choke = function (at) { | ||
stop(at + (0.02 * 6)) | ||
if (choker && at < playTo) { | ||
choker.gain.setTargetAtTime(0, at, 0.02) | ||
} | ||
} | ||
obs.connect = output.connect.bind(output) | ||
obs.disconnect = output.disconnect.bind(output) | ||
obs.triggerOn = function (at) { | ||
obs.choke(at) | ||
return obs | ||
// scoped | ||
function trigger (at) { | ||
var buffer = obs.resolvedBuffer() | ||
if (buffer instanceof window.AudioBuffer) { | ||
choker = context.audio.createGain() | ||
amp = context.audio.createGain() | ||
player = context.audio.createBufferSource() | ||
var choker = context.audio.createGain() | ||
var player = context.audio.createBufferSource() | ||
player.connect(choker) | ||
choker.connect(amp) | ||
amp.gain.value = 0 | ||
releaseAmp = Apply(context, amp.gain, obs.amp) | ||
player.connect(amp) | ||
amp.connect(choker) | ||
choker.connect(output) | ||
player.buffer = buffer | ||
player.loop = true | ||
player.start(at, 0) | ||
Param.triggerOn(obs, at) | ||
} | ||
} | ||
obs.triggerOff = function (at) { | ||
at = at || context.audio.currentTime | ||
var stopAt = obs.getReleaseDuration() + at | ||
Param.triggerOff(obs, stopAt) | ||
stop(stopAt) | ||
} | ||
obs.connect = output.connect.bind(output) | ||
obs.disconnect = output.disconnect.bind(output) | ||
return obs | ||
// scoped | ||
function stop (at) { | ||
if (player) { | ||
playTo = at | ||
player.stop(at) | ||
releaseAmp() | ||
return new ScheduleEvent(at, player, choker, [ | ||
choker.disconnect.bind(choker) | ||
]) | ||
} | ||
@@ -92,0 +61,0 @@ } |
@@ -1,9 +0,9 @@ | ||
var ObservStruct = require('observ-struct') | ||
var Triggerable = require('../triggerable') | ||
var Param = require('audio-slot-param') | ||
var Property = require('observ-default') | ||
var Transform = require('audio-slot-param/transform') | ||
var Apply = require('audio-slot-param/apply') | ||
var applyScheduler = require('../lib/apply-scheduler') | ||
var watch = require('observ/watch') | ||
var Property = require('observ-default') | ||
var ScheduleEvent = require('../lib/schedule-event') | ||
@@ -13,18 +13,10 @@ module.exports = OscillatorNode | ||
function OscillatorNode (context) { | ||
var oscillator = null | ||
var output = context.audio.createGain() | ||
var amp = context.audio.createGain() | ||
var power = context.audio.createGain() | ||
var amp = context.audio.createGain() | ||
var choker = context.audio.createGain() | ||
var output = context.audio.createGain() | ||
choker.gain.value = 0 | ||
amp.gain.value = 0 | ||
power.connect(amp) | ||
amp.connect(choker) | ||
amp.connect(output) | ||
var releaseSchedule = applyScheduler(context, handleSchedule) | ||
var releaseSync = [] | ||
var obs = ObservStruct({ | ||
var obs = Triggerable(context, { | ||
amp: Param(context, 1), | ||
@@ -36,8 +28,4 @@ frequency: Param(context, 440), | ||
shape: Property('sine') // Param(context, multiplier.gain, 1) | ||
}) | ||
}, trigger) | ||
var maxTime = null | ||
var lastOn = -1 | ||
var lastOff = 0 | ||
obs.context = context | ||
@@ -56,105 +44,40 @@ | ||
Apply(context, power.gain, powerRolloff) | ||
Apply(context, amp.gain, obs.amp) | ||
Apply(context, power.gain, powerRolloff) | ||
obs.shape(refreshShape) | ||
obs.getReleaseDuration = Param.getReleaseDuration.bind(this, obs) | ||
obs.choke = function (at) { | ||
if (choker) { | ||
choker.gain.setTargetAtTime(0, at, 0.02) | ||
} | ||
} | ||
obs.triggerOn = function (at) { | ||
at = at || context.audio.currentTime | ||
choker.connect(output) | ||
choker.gain.cancelScheduledValues(at) | ||
choker.gain.setValueAtTime(1, at) | ||
// start modulators | ||
Param.triggerOn(obs, at) | ||
maxTime = null | ||
if (lastOn < at) { | ||
lastOn = at | ||
} | ||
} | ||
obs.triggerOff = function (at) { | ||
at = at || context.audio.currentTime | ||
var stopAt = obs.getReleaseDuration() + at | ||
// stop modulators | ||
Param.triggerOff(obs, stopAt) | ||
choker.gain.setValueAtTime(0, stopAt) | ||
if (stopAt > maxTime) { | ||
maxTime = stopAt | ||
} | ||
if (lastOff < at) { | ||
lastOff = at | ||
} | ||
} | ||
obs.destroy = function () { | ||
// release context.noteOffset | ||
frequency.destroy() | ||
releaseSchedule && releaseSchedule() | ||
releaseSchedule = null | ||
} | ||
obs.connect = output.connect.bind(output) | ||
obs.disconnect = output.disconnect.bind(output) | ||
resync() | ||
return obs | ||
// | ||
// scoped | ||
function trigger (at) { | ||
var oscillator = context.audio.createOscillator() | ||
var choker = context.audio.createGain() | ||
oscillator.start(at) | ||
oscillator.connect(choker) | ||
choker.connect(amp) | ||
function handleSchedule (schedule) { | ||
if (maxTime && context.audio.currentTime > maxTime) { | ||
maxTime = null | ||
choker.disconnect() | ||
resync() | ||
} | ||
return new ScheduleEvent(at, oscillator, choker, [ | ||
Apply(context, oscillator.detune, obs.detune), | ||
Apply(context, oscillator.frequency, frequency), | ||
ApplyShape(context, oscillator, obs.shape), | ||
choker.disconnect.bind(choker) | ||
]) | ||
} | ||
} | ||
function resync () { | ||
while (releaseSync.length) { | ||
releaseSync.pop()() | ||
} | ||
function ApplyShape (context, target, shape) { | ||
return watch(shape, setShape.bind(this, context, target)) | ||
} | ||
if (oscillator) { | ||
oscillator.disconnect() | ||
function setShape (context, target, value) { | ||
if (value !== target.lastShape) { | ||
if (context.periodicWaves && context.periodicWaves[value]) { | ||
target.setPeriodicWave(context.periodicWaves[value]) | ||
} else { | ||
target.type = value | ||
} | ||
oscillator = context.audio.createOscillator() | ||
oscillator.lastShape = 'sine' | ||
refreshShape() | ||
oscillator.connect(power) | ||
oscillator.start() | ||
releaseSync.push( | ||
Apply(context, oscillator.detune, obs.detune), | ||
Apply(context, oscillator.frequency, frequency) | ||
) | ||
target.lastShape = value | ||
} | ||
function refreshShape () { | ||
var shape = obs.shape() | ||
if (shape !== oscillator.lastShape) { | ||
if (context.periodicWaves && context.periodicWaves[shape]) { | ||
oscillator.setPeriodicWave(context.periodicWaves[shape]) | ||
} else { | ||
oscillator.type = shape | ||
} | ||
oscillator.lastShape = shape | ||
} | ||
} | ||
} | ||
@@ -161,0 +84,0 @@ |
@@ -1,10 +0,11 @@ | ||
var ObservStruct = require('observ-struct') | ||
var Node = require('observ-node-array/single') | ||
var ResolvedValue = require('observ-node-array/resolved-value') | ||
var Param = require('audio-slot-param') | ||
var Property = require('observ-default') | ||
var Param = require('audio-slot-param') | ||
var Transform = require('audio-slot-param/transform') | ||
var Apply = require('audio-slot-param/apply') | ||
var watch = require('observ/watch') | ||
var ResolvedValue = require('observ-node-array/resolved-value') | ||
var Triggerable = require('../triggerable') | ||
var ScheduleEvent = require('../lib/schedule-event') | ||
@@ -15,4 +16,7 @@ module.exports = SampleNode | ||
var output = context.audio.createGain() | ||
var amp = context.audio.createGain() | ||
amp.gain.value = 0 | ||
amp.connect(output) | ||
var obs = ObservStruct({ | ||
var obs = Triggerable(context, { | ||
mode: Property('hold'), | ||
@@ -25,20 +29,7 @@ offset: Property([0, 1]), | ||
tune: Param(context, 0) | ||
}, trigger) | ||
}) | ||
obs.resolvedBuffer = ResolvedValue(obs.buffer) | ||
obs.context = context | ||
var player = null | ||
var choker = null | ||
var amp = null | ||
var lastChoke = 0 | ||
var releases = [] | ||
var playTo = false | ||
var triggerOnRelease = false | ||
var isOneshot = false | ||
var playbackRate = Transform(context, [ 1, | ||
@@ -50,119 +41,51 @@ { param: context.noteOffset, transform: noteOffsetToRate }, | ||
obs.offset(function (value) { | ||
var buffer = obs.resolvedBuffer() | ||
if (buffer && player && Array.isArray(value)) { | ||
player.loopStart = buffer.duration * value[0] | ||
player.loopEnd = buffer.duration * value[1] | ||
} | ||
}) | ||
Apply(context, amp.gain, obs.amp) | ||
obs.choke = function (at) { | ||
stop(at + (0.02 * 6)) | ||
if (choker && at < playTo) { | ||
lastChoke = at | ||
choker.gain.setTargetAtTime(0, at, 0.02) | ||
} | ||
obs.connect = output.connect.bind(output) | ||
obs.disconnect = output.disconnect.bind(output) | ||
// clean up | ||
while (releases.length) { | ||
releases.pop()() | ||
} | ||
} | ||
return obs | ||
obs.triggerOn = function (at) { | ||
obs.choke(at) | ||
// scoped | ||
function trigger (at) { | ||
var buffer = obs.resolvedBuffer() | ||
var mode = obs.mode() | ||
var buffer = obs.resolvedBuffer() | ||
if (buffer instanceof window.AudioBuffer) { | ||
playTo = null | ||
choker = context.audio.createGain() | ||
amp = context.audio.createGain() | ||
player = context.audio.createBufferSource() | ||
var choker = context.audio.createGain() | ||
var player = context.audio.createBufferSource() | ||
player.connect(choker) | ||
choker.connect(amp) | ||
releases.push( | ||
Apply(context, amp.gain, obs.amp), | ||
Apply(context, player.playbackRate, playbackRate) | ||
) | ||
// nice fade on retrigger (after choke) | ||
if (Math.abs(at - lastChoke) < 0.02) { | ||
choker.gain.setValueAtTime(0, at) | ||
choker.gain.setTargetAtTime(1, at, 0.001) | ||
} | ||
player.connect(amp) | ||
amp.connect(choker) | ||
choker.connect(output) | ||
player.buffer = buffer | ||
player.loopStart = buffer.duration * obs.offset()[0] | ||
player.loopEnd = buffer.duration * obs.offset()[1] | ||
player.onended = disconnectSelf | ||
if (mode === 'loop') { | ||
player.loop = true | ||
player.start(at, player.loopStart, 1000) | ||
Param.triggerOn(obs, at) | ||
} else if (mode === 'release') { | ||
triggerOnRelease = true | ||
} else if (mode === 'oneshot') { | ||
var event = new ScheduleEvent(at, player, choker, [ | ||
Apply(context, player.playbackRate, playbackRate), | ||
ApplyLoopMode(context, player, obs.mode), | ||
choker.disconnect.bind(choker) | ||
]) | ||
event.to = at + (player.loopEnd - player.loopStart) / playbackRate.getValueAt(at) | ||
if (mode !== 'release') { | ||
player.start(at, player.loopStart) | ||
Param.triggerOn(obs, at) | ||
var duration = (player.loopEnd - player.loopStart) / playbackRate.getValueAt(at) | ||
var maxDuration = buffer.duration - player.loopStart / playbackRate.getValueAt(at) | ||
var extra = Math.max(0, obs.getReleaseDuration() - maxDuration) | ||
obs.triggerOff(at + duration - extra) | ||
isOneshot = true | ||
} else { | ||
player.start(at, player.loopStart, player.loopEnd - player.loopStart) | ||
Param.triggerOn(obs, at) | ||
} | ||
} | ||
} | ||
obs.triggerOff = function (at) { | ||
if (isOneshot) { | ||
isOneshot = false | ||
} else { | ||
at = at || context.audio.currentTime | ||
var stopAt = obs.getReleaseDuration() + at | ||
if (triggerOnRelease) { | ||
Param.triggerOn(obs, at) | ||
player.start(at, player.loopStart, player.loopEnd - player.loopStart) | ||
stopAt += player.loopEnd - player.loopStart | ||
triggerOnRelease = false | ||
if (mode === 'oneshot') { | ||
event.oneshot = true | ||
} | ||
Param.triggerOff(obs, stopAt) | ||
stop(stopAt) | ||
return event | ||
} | ||
} | ||
} | ||
obs.destroy = function () { | ||
// release context.noteOffset | ||
playbackRate.destroy() | ||
} | ||
obs.getReleaseDuration = Param.getReleaseDuration.bind(this, obs) | ||
obs.connect = output.connect.bind(output) | ||
obs.disconnect = output.disconnect.bind(output) | ||
return obs | ||
// scoped | ||
function stop (at) { | ||
if ((!playTo || at < playTo) && player) { | ||
playTo = at | ||
player.stop(at) | ||
} | ||
} | ||
function ApplyLoopMode (context, target, mode) { | ||
return watch(mode, setLoopMode.bind(this, context, target)) | ||
} | ||
function disconnectSelf () { | ||
this.disconnect() | ||
function setLoopMode (context, target, mode) { | ||
target.loop = mode === 'loop' | ||
} | ||
@@ -169,0 +92,0 @@ |
98287
48
3009
14
+ Addedmake-distortion-curve@^1.0.0
+ Addednoise-buffer@^2.0.0
+ Addedaudio-slot-param@2.2.5(transitive)
+ Addedmake-distortion-curve@1.0.0(transitive)
+ Addednoise-buffer@2.0.1(transitive)
- Removedaudio-slot-param@1.1.4(transitive)
Updatedaudio-slot-param@~2.2.0
Updatedobserv-node-array@^1.11.1