Skip to content

Commit d5b2d39

Browse files
feat(initial): added dependencies and initial code
1 parent db4bf08 commit d5b2d39

10 files changed

+367
-0
lines changed

Diff for: .babelrc

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"presets": [
3+
[
4+
"@babel/preset-env", { "modules": false }
5+
]
6+
]
7+
}

Diff for: .eslintrc

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"extends": "standard",
3+
"rules": {
4+
"no-var": "error",
5+
"prefer-const": "error"
6+
}
7+
}

Diff for: .gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules
2+
package-lock.json
3+
.vscode

Diff for: .travis.yml

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
language: node_js
2+
cache:
3+
directories:
4+
- node_modules
5+
notifications:
6+
email: false
7+
node_js:
8+
- 'node'
9+
- '8'
10+
before_script:
11+
- npm prune
12+
after_success:
13+
- npm run semantic-release
14+
branches:
15+
except:
16+
- /^v\d+\.\d+\.\d+$/

Diff for: README.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# AudioParam.getValueAtTime()
2+
3+
This code monkey patches AudioParams with a method to get values at any time

Diff for: dist/audioparam-getvalueattime.min.js

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: package.json

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
{
2+
"name": "audioparam-getvalueattime",
3+
"version": "0.0.0-development",
4+
"description": "This code monkey patches AudioParams with a method to get values at any time",
5+
"main": "src/index.js",
6+
"scripts": {
7+
"build": "rollup -c",
8+
"build:watch": "npm run build -- --watch",
9+
"lint": "eslint src --fix",
10+
"stage-after-lint": "git add src",
11+
"commit": "git-cz",
12+
"commit:again": "git-cz --retry",
13+
"semantic-release": "semantic-release",
14+
"dist:clean": "rimraf dist || true",
15+
"dist:stage": "git add dist --force"
16+
},
17+
"pre-commit": [
18+
"lint",
19+
"stage-after-lint",
20+
"dist:clean",
21+
"build",
22+
"dist:stage"
23+
],
24+
"repository": {
25+
"type": "git",
26+
"url": "git+https://github.com/the-monochord/AudioParam.getValueAtTime.git"
27+
},
28+
"author": "Lajos Meszaros <[email protected]>",
29+
"license": "GPL-3.0-or-later",
30+
"bugs": {
31+
"url": "https://github.com/the-monochord/AudioParam.getValueAtTime/issues"
32+
},
33+
"homepage": "https://github.com/the-monochord/AudioParam.getValueAtTime#readme",
34+
"devDependencies": {
35+
"@babel/core": "^7.6.0",
36+
"@babel/preset-env": "^7.6.0",
37+
"cz-conventional-changelog": "^3.0.2",
38+
"eslint": "^6.4.0",
39+
"eslint-config-standard": "^14.1.0",
40+
"eslint-plugin-import": "^2.18.2",
41+
"eslint-plugin-node": "^10.0.0",
42+
"eslint-plugin-promise": "^4.2.1",
43+
"eslint-plugin-standard": "^4.0.1",
44+
"pre-commit": "^1.2.2",
45+
"rimraf": "^3.0.0",
46+
"rollup": "^1.21.4",
47+
"rollup-plugin-babel": "^4.3.3",
48+
"rollup-plugin-commonjs": "^10.1.0",
49+
"rollup-plugin-node-resolve": "^5.2.0",
50+
"rollup-plugin-ramda": "^1.0.5",
51+
"rollup-plugin-terser": "^5.1.2",
52+
"semantic-release": "^15.13.24"
53+
},
54+
"dependencies": {
55+
"pseudo-audio-param": "^1.3.1",
56+
"ramda": "^0.26.1"
57+
},
58+
"config": {
59+
"commitizen": {
60+
"path": "./node_modules/cz-conventional-changelog"
61+
}
62+
}
63+
}

Diff for: rollup.config.js

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { terser } from 'rollup-plugin-terser'
2+
import babel from 'rollup-plugin-babel'
3+
import commonjs from 'rollup-plugin-commonjs'
4+
import resolve from 'rollup-plugin-node-resolve'
5+
import ramda from 'rollup-plugin-ramda'
6+
import fs from 'fs'
7+
8+
const getDate = () => {
9+
const d = new Date()
10+
const year = d.getFullYear()
11+
const month = (d.getMonth() + 1 < 10 ? '0' : '') + (d.getMonth() + 1)
12+
const day = (d.getDate() < 10 ? '0' : '') + d.getDate()
13+
return `${year}-${month}-${day}`
14+
}
15+
16+
const config = JSON.parse(fs.readFileSync('package.json'))
17+
const banner = `// ${config.name} - created by ${config.author} - ${config.license} licence - last built on ${getDate()}`
18+
19+
export default [{
20+
input: 'src/index.js',
21+
output: {
22+
file: 'dist/audioparam-getvalueattime.min.js',
23+
format: 'iife',
24+
sourcemap: false
25+
},
26+
plugins: [
27+
resolve({
28+
mainFields: ['jsnext', 'main']
29+
}),
30+
commonjs({
31+
namedExports: {
32+
'node_modules/ramda/index.js': Object.keys(require('ramda'))
33+
}
34+
}),
35+
ramda(),
36+
babel(),
37+
terser({
38+
mangle: false,
39+
output: {
40+
preamble: banner
41+
}
42+
})
43+
]
44+
}]

Diff for: src/helpers.js

+185
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
/* global BaseAudioContext, AudioContext, webkitAudioContext, AudioParam */
2+
3+
import { isEmpty, prop, compose, not, clamp, isNil, reject, append, equals, lt, __, gte, either, filter, both, reduce, max, pluck, unless, find, propEq, min, gt, last, has, all, props, add } from 'ramda'
4+
import { getLinearRampToValueAtTime, getExponentialRampToValueAtTime, getTargetValueAtTime, getValueCurveAtTime } from 'pseudo-audio-param/lib/expr.js'
5+
6+
const AudioContextClass = isNil(window.BaseAudioContext) ? (isNil(window.AudioContext) ? webkitAudioContext : AudioContext) : BaseAudioContext
7+
8+
const maxAll = reduce(max, -Infinity)
9+
const minAll = reduce(min, Infinity)
10+
11+
const findLastChangeBeforeTime = (scheduledChanges, time) => {
12+
const targetTimeOfLastChange = compose(
13+
maxAll,
14+
filter(lt(__, time)),
15+
pluck('targetTime')
16+
)(scheduledChanges)
17+
18+
return find(propEq('targetTime', targetTimeOfLastChange), scheduledChanges)
19+
}
20+
21+
const findFirstChangeAfterTime = (scheduledChanges, time) => {
22+
const targetTimeOfLastChange = compose(
23+
minAll,
24+
filter(gt(__, time)),
25+
pluck('targetTime')
26+
)(scheduledChanges)
27+
28+
return find(propEq('targetTime', targetTimeOfLastChange), scheduledChanges)
29+
}
30+
31+
const getTargetValueOfChange = scheduledChange => {
32+
if (scheduledChange.method === 'setValueCurveAtTime') {
33+
return last(scheduledChange.params[0])
34+
} else {
35+
return scheduledChange.params[0]
36+
}
37+
}
38+
39+
const evaluateSchedulement = (scheduledChanges, initialValue, initialTime, endTime = Infinity) => {
40+
const lastChangeBeforeTime = findLastChangeBeforeTime(scheduledChanges, endTime)
41+
const firstChangeAfterTime = findFirstChangeAfterTime(scheduledChanges, endTime)
42+
43+
let value = isNil(lastChangeBeforeTime) ? initialValue : getTargetValueOfChange(lastChangeBeforeTime)
44+
if (!isNil(firstChangeAfterTime)) {
45+
const endTimeOfLastChange = isNil(lastChangeBeforeTime) ? initialTime : lastChangeBeforeTime.targetTime
46+
switch (firstChangeAfterTime.method) {
47+
case 'linearRampToValueAtTime':
48+
value = getLinearRampToValueAtTime(endTime, value, getTargetValueOfChange(firstChangeAfterTime), endTimeOfLastChange, firstChangeAfterTime.targetTime)
49+
break
50+
case 'exponentialRampToValueAtTime':
51+
value = getExponentialRampToValueAtTime(endTime, value, getTargetValueOfChange(firstChangeAfterTime), endTimeOfLastChange, firstChangeAfterTime.targetTime)
52+
break
53+
case 'setTargetAtTime':
54+
value = getTargetValueAtTime(endTime, value, firstChangeAfterTime.params[0], firstChangeAfterTime.params[1], firstChangeAfterTime.params[2])
55+
break
56+
case 'setValueCurveAtTime':
57+
value = getValueCurveAtTime(endTime, firstChangeAfterTime.params[0], firstChangeAfterTime.params[1], firstChangeAfterTime.params[2])
58+
break
59+
}
60+
}
61+
62+
return value
63+
}
64+
65+
const scheduleChange = (audioParam, method, params, targetTime) => {
66+
const now = audioParam._ctx.currentTime
67+
68+
const outdatedSchedulements = filter(compose(
69+
both(
70+
gte(__, audioParam._valueWasLastSetAt),
71+
lt(__, now)
72+
),
73+
prop('targetTime')
74+
))(audioParam._scheduledChanges)
75+
76+
if (!isEmpty(outdatedSchedulements)) {
77+
audioParam._valueWasLastSetAt = compose(
78+
maxAll,
79+
pluck('targetTime')
80+
)(outdatedSchedulements)
81+
audioParam._value = evaluateSchedulement(outdatedSchedulements, audioParam._value, audioParam._valueWasLastSetAt)
82+
}
83+
84+
audioParam._scheduledChanges = compose(
85+
unless(
86+
() => method === 'cancelScheduledValues',
87+
append({
88+
method,
89+
params,
90+
targetTime: clamp(now, Infinity, targetTime)
91+
})
92+
),
93+
reject(compose(
94+
either(
95+
(method === 'cancelScheduledValues' ? gte(__, targetTime) : equals(__, targetTime)),
96+
lt(__, now)
97+
),
98+
prop('targetTime')
99+
))
100+
)(audioParam._scheduledChanges)
101+
}
102+
103+
// gotChangesScheduled :: audioParam -> bool
104+
const gotChangesScheduled = compose(
105+
not,
106+
isEmpty,
107+
prop('_scheduledChanges')
108+
)
109+
110+
const getValueAtTime = (audioParam, time) => {
111+
if (gotChangesScheduled(audioParam)) {
112+
return evaluateSchedulement(audioParam._scheduledChanges, audioParam._value, audioParam._valueWasLastSetAt, time)
113+
} else {
114+
return audioParam._value
115+
}
116+
}
117+
118+
// The AudioContext, on which the createX function was called is not accessible from the created AudioNode's params.
119+
// This will bind the AudioContext to the AudioParam's _ctx property.
120+
//
121+
// Example:
122+
// const osc = ctx.createOscillator()
123+
// console.log(osc.frequency._ctx === ctx) // true
124+
const bindContextToParams = (creatorName, params) => {
125+
const originalFn = AudioContextClass.prototype[creatorName]
126+
if (!isNil(originalFn)) {
127+
AudioContextClass.prototype[creatorName] = function (...args) {
128+
const ctx = this
129+
const node = originalFn.apply(ctx, args)
130+
params.forEach(param => {
131+
const audioParam = node[param]
132+
audioParam._ctx = ctx
133+
audioParam._value = audioParam.value
134+
audioParam._valueWasLastSetAt = 0
135+
audioParam._scheduledChanges = []
136+
137+
// ramps don't take effect, until there was at least one scheduled change
138+
// audioParam._hadFinishedSchedulement = false // TODO: when to set this to true?
139+
})
140+
return node
141+
}
142+
}
143+
}
144+
145+
const bindSchedulerToParamMethod = (methodName, timeArgIndexes = []) => {
146+
const originalFn = AudioParam.prototype[methodName]
147+
if (!isNil(originalFn)) {
148+
AudioParam.prototype[methodName] = function (...args) {
149+
const audioParam = this
150+
let targetTime = Infinity
151+
if (!isEmpty(timeArgIndexes) && all(has(__, args), timeArgIndexes)) {
152+
targetTime = reduce(add, 0, props(timeArgIndexes, args))
153+
}
154+
scheduleChange(audioParam, methodName, args, targetTime)
155+
originalFn.apply(audioParam, args)
156+
return audioParam
157+
}
158+
}
159+
}
160+
161+
// older Firefox versions always return the defaultValue when reading the value from an AudioParam
162+
// the correct current value can be read from audioParam._value
163+
const hijackParamValueSetter = () => {
164+
const descriptor = Object.getOwnPropertyDescriptor(AudioParam.prototype, 'value')
165+
const originalSetter = descriptor.set
166+
descriptor.set = function (newValue) {
167+
const audioParam = this
168+
// value change gets ignored in Firefox and Safari, if there are changes scheduled
169+
if (!gotChangesScheduled(audioParam)) {
170+
audioParam._value = clamp(audioParam.minValue, audioParam.maxValue, newValue)
171+
audioParam._valueWasLastSetAt = audioParam._ctx.currentTime
172+
originalSetter.call(audioParam, newValue)
173+
}
174+
}
175+
Object.defineProperty(AudioParam.prototype, 'value', descriptor)
176+
}
177+
178+
export {
179+
scheduleChange,
180+
gotChangesScheduled,
181+
getValueAtTime,
182+
bindContextToParams,
183+
bindSchedulerToParamMethod,
184+
hijackParamValueSetter
185+
}

Diff for: src/index.js

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/* global AudioParam */
2+
3+
import { isNil } from 'ramda'
4+
import {
5+
getValueAtTime,
6+
bindContextToParams,
7+
bindSchedulerToParamMethod,
8+
hijackParamValueSetter
9+
} from './helpers'
10+
11+
if (!isNil(window.AudioParam) && isNil(AudioParam.prototype.getValueAtTime)) {
12+
// bind all the create* functions, which create objects with at least 1 AudioParam among their properties:
13+
bindContextToParams('createBiquadFilter', ['frequency', 'detune', 'Q', 'gain'])
14+
bindContextToParams('createBufferSource', ['detune', 'playbackRate'])
15+
bindContextToParams('createConstantSource', ['offset'])
16+
bindContextToParams('createDelay', ['delayTime'])
17+
bindContextToParams('createDynamicsCompressor', ['threshold', 'knee', 'ratio', 'attack', 'release'])
18+
bindContextToParams('createGain', ['gain'])
19+
bindContextToParams('createOscillator', ['frequency', 'detune'])
20+
bindContextToParams('createPanner', ['orientationX', 'orientationY', 'orientationZ', 'positionX', 'positionY', 'positionZ'])
21+
bindContextToParams('createStereoPanner', ['pan'])
22+
23+
// hijack param methods and mark which argument has the time
24+
bindSchedulerToParamMethod('cancelScheduledValues', [0])
25+
bindSchedulerToParamMethod('setValueAtTime', [1])
26+
bindSchedulerToParamMethod('linearRampToValueAtTime', [1])
27+
bindSchedulerToParamMethod('exponentialRampToValueAtTime', [1])
28+
bindSchedulerToParamMethod('setTargetAtTime', [])
29+
bindSchedulerToParamMethod('setValueCurveAtTime', [1, 2])
30+
31+
hijackParamValueSetter()
32+
33+
AudioParam.prototype.getValueAtTime = function (time) {
34+
const audioParam = this
35+
return getValueAtTime(audioParam, time)
36+
}
37+
}

0 commit comments

Comments
 (0)