Skip to content
Permalink
Browse files

use linear envelope attack, add sustain toggle on slots

closes #217
  • Loading branch information...
mmckegg committed Nov 7, 2017
1 parent 167707b commit bcc78a419cacf85b9d2060f0f2cc226d8d5e512c
@@ -1,18 +1,18 @@
var PseudoAudioParam = require('pseudo-audio-param')
var Event = require('geval')
var extend = require('xtend')
var truncateTypes = ['linearRampToValueAtTime', 'exponentialRampToValueAtTime']

module.exports = ParamSource

function ParamSource (context, defaultValue) {
var result = new PseudoAudioParam(defaultValue)
var insertEvent = result._insertEvent.bind(result) // HACK: monkey patch events

result.context = context

result.onEvent = Event(function (broadcast) {
result._insertEvent = function (event) {
insertEvent(event)
insertEvent(result, event)
broadcast(event)
}
result.cancelScheduledValues = function (time) {
@@ -23,6 +23,27 @@ function ParamSource (context, defaultValue) {
args: [time]
})
}
result.cancelAndHoldAtTime = function (time) {
var truncatedEvent = null
for (var i = result.events.length - 1; i >= 0; i--) {
if (time <= result.events[i].time) {
truncatedEvent = result.events[i]
} else {
break
}
}

if (truncatedEvent) {
var value = result.getValueAtTime(time)
result.cancelScheduledValues(time)

if (truncatedEvent && truncateTypes.includes(truncatedEvent.type)) {
result[truncatedEvent.type](value, time)
}

result.setValueAtTime(value, time)
}
}
})

return result
@@ -153,7 +174,7 @@ function getValueAt (params, at, reducer) {
}

function cancelScheduledValues (source, time) {
while (source.events.length && last(source.events).time < time) {
while (source.events.length && time <= last(source.events).time) {
source.events.pop()
}
if (time < source._prevGotTime) {
@@ -166,3 +187,26 @@ function cancelScheduledValues (source, time) {
function last (array) {
return array[array.length - 1]
}

function insertEvent (param, eventItem) {
var time = eventItem.time;
var events = param.events;
var replace = 0;
var i, imax;

if (events.length === 0 || events[events.length - 1].time < time) {
events.push(eventItem);
} else {
for (i = 0, imax = events.length; i < imax; i++) {
if (events[i].time === time && events[i].type === eventItem.type) {
replace = 1;
break;
}
if (time < events[i].time) {
break;
}
}
param._eventIndex = 0;
events.splice(replace ? i : i + 1, replace, eventItem);
}
};
@@ -77,6 +77,10 @@ function Param (parentContext, defaultValue) {
return obs.node && obs.node.triggerOff && obs.node.triggerOff(at) || 0
}

obs.getAttackDuration = function () {
return obs.node && obs.node.getAttackDuration && obs.node.getAttackDuration() || 0
}

obs.getReleaseDuration = function () {
return obs.node && obs.node.getReleaseDuration && obs.node.getReleaseDuration() || 0
}
@@ -108,6 +112,19 @@ Param.triggerOff = function (obj, stopAt) {
}
}

Param.getAttackDuration = function (obj) {
var result = 0
for (var k in obj) {
if (obj[k] && obj[k].getAttackDuration) {
var val = obj[k].getAttackDuration()
if (val > result) {
result = val
}
}
}
return result
}

Param.getReleaseDuration = function (obj) {
var result = 0
for (var k in obj) {
@@ -11,6 +11,8 @@ function ProcessorNode (context, input, output, params, releases) {
obs.connect = output.connect.bind(output)
obs.disconnect = output.disconnect.bind(output)
obs.getReleaseDuration = Param.getReleaseDuration.bind(this, obs)
obs.getAttackDuration = Param.getAttackDuration.bind(this, obs)

obs.context = context

obs.triggerOn = function (at) {
@@ -7,6 +7,7 @@ function Triggerable (context, params, trigger, releases) {
var obs = ObservStruct(params)
var lastEvent = null

obs.getAttackDuration = Param.getAttackDuration.bind(this, obs)
obs.getReleaseDuration = Param.getReleaseDuration.bind(this, obs)

obs.triggerOn = function (at) {
@@ -17,33 +17,31 @@ function Envelope (context) {
value: Param(context, 1)
})


var outputParam = ParamSource(context, 0)
obs.currentValue = outputParam// Multiply([obs.value, outputParam])
obs.context = context
obs.id.context = context

obs.triggerOn = function (at) {
at = Math.max(at, context.audio.currentTime)
outputParam.cancelScheduledValues(at)
outputParam.cancelAndHoldAtTime(at)

if (obs.retrigger()) {
outputParam.setValueAtTime(0, at)
}
var startValue = obs.retrigger() ? 0 : outputParam.getValueAtTime(at)
outputParam.setValueAtTime(startValue, at)

Param.triggerOn(obs, at)

var attackTime = obs.attack.getValueAtTime(at) || 0.005
var decayTime = obs.decay.getValueAtTime(at) || 0.005
var peakTime = at + attackTime
var attackDuration = obs.attack.getValueAtTime(at) || 0.005
var decayDuration = obs.decay.getValueAtTime(at) || 0.005
var peakTime = at + attackDuration
var value = obs.value.getValueAtTime(at)

outputParam.setTargetAtTime(value, at, attackTime / 8)
outputParam.linearRampToValueAtTime(value, peakTime)

// decay / sustain
var sustain = obs.sustain.getValueAtTime(at) * value
if (sustain !== 1) {
outputParam.setTargetAtTime(sustain, peakTime, decayTime / 8)
if (sustain !== value) {
outputParam.linearRampToValueAtTime(sustain, peakTime + decayDuration)
}
}

@@ -54,7 +52,8 @@ function Envelope (context) {
var stopAt = at + releaseTime

Param.triggerOff(obs, stopAt)
outputParam.cancelScheduledValues(at)
outputParam.cancelAndHoldAtTime(at)

outputParam.setTargetAtTime(0, at, releaseTime / 8)

// HACK: clean up hanging target
@@ -63,6 +62,10 @@ function Envelope (context) {
return stopAt
}

obs.getAttackDuration = function () {
return obs.attack.getValueAtTime(context.audio.currentTime) || 0.005
}

obs.getReleaseDuration = function () {
return obs.release.getValueAtTime(context.audio.currentTime)
}
@@ -32,6 +32,7 @@ function LFO (context) {
})

obs.context = context
obs.getAttackDuration = Param.getAttackDuration.bind(this, obs)
obs.getReleaseDuration = Param.getReleaseDuration.bind(this, obs)
obs.id.context = context

@@ -36,10 +36,24 @@ function AudioSlot (parentContext, defaultValue) {
sources: Slots(context),
processors: Slots(context),
noteOffset: Param(context, 0),
sustain: Property(true),
output: Property(null),
volume: Property(1)
}, input, output, releases)

obs.getAttackDuration = function () {
var duration = 0
forEachAll([obs.sources, obs.modulators, obs.processors], function (node) {
if (node && node.getAttackDuration) {
var value = node.getAttackDuration()
if (value && (value > duration)) {
duration = value
}
}
})
return duration || 0.0001
}

obs._type = 'AudioSlot'
context.noteOffset = obs.noteOffset
context.slot = obs
@@ -124,18 +138,56 @@ function AudioSlot (parentContext, defaultValue) {
}

if (offTime) {
obs.triggerOff(offTime)
triggerOff(offTime)
} else if (!obs.sustain()) {
triggerOff(at + obs.getAttackDuration())
}
}

obs.triggerOff = function (at) {
if (!obs.sustain()) return // ignore triggerOff

if (!initialized) {
queue.push(function () {
obs.triggerOff(at)
})
return false
}

triggerOff(at)
}

obs.choke = function (at) {
obs.sources.forEach(function (source) {
source.choke && source.choke(at)
})
}

releases.push(
function () {
if (isOn()) {
// force trigger off on removal
obs.triggerOff(context.audio.currentTime)
}
}
)

if (defaultValue) {
obs.set(defaultValue)
}

setImmediate(function () {
initialized = true
while (queue.length) {
queue.shift()()
}
})

return obs

// scoped

function triggerOff (at) {
var maxProcessorDuration = 0
var maxSourceDuration = 0

@@ -181,36 +233,6 @@ function AudioSlot (parentContext, defaultValue) {
}
}

obs.choke = function (at) {
obs.sources.forEach(function (source) {
source.choke && source.choke(at)
})
}

releases.push(
function () {
if (isOn()) {
// force trigger off on removal
obs.triggerOff(context.audio.currentTime)
}
}
)

if (defaultValue) {
obs.set(defaultValue)
}

setImmediate(function () {
initialized = true
while (queue.length) {
queue.shift()()
}
})

return obs

// scoped

function triggerIfOn (node) {
if (isOn() && node.triggerOn) {
// immediately trigger processors if slot is already triggered
@@ -2,7 +2,7 @@ var h = require('lib/h')
var Collection = require('lib/widgets/collection')
var Spawner = require('lib/widgets/spawner')
var Range = require('lib/params/range')
var Select = require('lib/params/select')
var ToggleButton = require('lib/params/toggle-button')
var QueryParam = require('lib/query-param')
var ToggleChooser = require('lib/params/toggle-chooser')

@@ -27,11 +27,14 @@ module.exports = function renderSlot (node) {
// NOTE: this check could be error prone - consider revising?
checkIsTrigger(node) ? [
h('section', [
h('div', {style: {'display': 'flex', 'align-items': 'center', 'flex': '1'}}, [
h('header', [
h('h1', 'Sources'),
ToggleChooser(QueryParam(node, 'chokeGroup'), {
title: 'Choke Group',
options: [['None', null], 'A', 'B', 'C', 'D']
}),
ToggleButton(node.sustain, {
title: 'Sustain'
})
]),

@@ -41,6 +41,7 @@ function ValueModulator (parentContext) {
return stopAt
}

obs.getAttackDuration = Param.getAttackDuration.bind(this, obs)
obs.getReleaseDuration = Param.getReleaseDuration.bind(this, obs)

obs.destroy = function () {
@@ -49,7 +49,7 @@
"observ-grid": "~2.10.1",
"observ-grid-stack": "~2.0.1",
"observ-midi": "~2.3.0",
"pseudo-audio-param": "^1.1.0",
"pseudo-audio-param": "^1.2.0",
"pull-cat": "^1.1.11",
"pull-stream": "^3.4.5",
"pull-stream-to-stream": "github:mmckegg/pull-stream-to-stream#e436acee18b71af8e71d1b5d32eee642351517c7",
@@ -1,5 +1,12 @@
AudioSlot {
section {
header {
display: flex
align-items: center
button {
margin-left: 10px
}
}
border-radius: 3px
background: #333
padding: 6px 8px
@@ -8,4 +15,4 @@ AudioSlot {
section + section {
margin-top: 5px
}
}
}

0 comments on commit bcc78a4

Please sign in to comment.
You can’t perform that action at this time.