1 /* Copyright 2014 Yukkuri Games
2 Licensed under the terms of the GNU GPL v2 or later
3 @license https://www.gnu.org/licenses/gpl-2.0.html
4 @source: https://yukkurigames.com/yuu/
10 var yT
= this.yT
|| require("./yT");
11 var yf
= this.yf
|| require("./yf");
14 /** Audio context/source/buffer accessor
16 You probably don't need to make this yourself; one is made
17 named yuu.audio during initialization.
19 You can set the master volume with yuu.audio.masterVolume.
21 constructor: function () {
22 this._ctx
= new window
.AudioContext();
23 this._compressor
= this._ctx
.createDynamicsCompressor();
24 this._masterVolume
= this._ctx
.createGain();
25 this._masterVolume
.connect(this._compressor
);
26 this._compressor
.connect(this._ctx
.destination
);
27 this._musicVolume
= this._ctx
.createGain();
28 this._musicVolume
.connect(this._masterVolume
);
29 this._masterVolume
.gain
.value
= 0.5;
30 this._musicVolume
.gain
.value
= 0.5;
32 this._bufferCache
= {};
35 this._volume
= this._masterVolume
.gain
.value
;
38 destination
: { alias
: "_masterVolume", readonly
: true },
39 music
: { alias
: "_musicVolume", readonly
: true },
41 _readStorage: function () {
44 yf
.each
.call(this, function (prop
) {
45 this[prop
] = this._storage
.getObject(prop
, this[prop
]);
46 }, ["volume", "musicVolume", "mute"]);
49 _writeStorage
: yf
.debounce(function () {
52 yf
.each
.call(this, function (prop
) {
53 this._storage
.setObject(prop
, this[prop
]);
54 }, ["volume", "musicVolume", "mute"]);
58 get: function () { return this._storage
; },
66 get: function () { return this._mute
; },
69 this.volume
= this.volume
;
74 get: function () { return this._volume
; },
77 v
= this._mute
? 0 : v
;
78 this._masterVolume
.gain
.value
= v
;
84 get: function () { return this._musicVolume
.gain
.value
; },
86 this._musicVolume
.gain
.value
= v
;
91 currentTime
: { alias
: "_ctx.currentTime" },
93 decodeAudioData: function (data
) {
96 return ctx
.decodeAudioData(data
);
98 return new Promise(function (resolve
, reject
) {
99 ctx
.decodeAudioData(data
, function (buffer
) {
102 reject(new Error("Error decoding audio buffer"));
108 createBufferSource: function (path
) {
109 var source
= this._ctx
.createBufferSource();
110 var sample
= yf
.isString(path
)
111 ? new yuu
.AudioSample(path
, this)
113 if ((source
.buffer
= sample
.buffer
) === null) {
114 sample
.ready
.then(function () {
115 source
.buffer
= sample
.buffer
;
121 sampleRate
: { alias
: "_ctx.sampleRate" },
122 createGain
: { proxy
: "_ctx.createGain" },
123 createOscillator
: { proxy
: "_ctx.createOscillator" },
126 // FIXME: This parsing is garbagey, would be better to parse when
127 // first handed a dfn and turn everything into a function.
128 function applyMod (s
, v
) {
129 if (yf
.isFunction(s
))
133 else if (s
[0] === "-" || s
[0] === "+")
135 else if (s
[0] === "x" || s
[0] === "*")
136 return v
* +s
.substring(1);
137 else if (s
[s
.length
- 1] === "%")
138 return v
* (parseFloat(s
) / 100);
143 var Envelope
= yuu
.Envelope
= yT({
144 constructor: yf
.argcd(
146 Envelope
.call(this, pairs
, 1);
148 function (pairs
, scale
) {
149 pairs
= pairs
|| { "0": 1, "100%": 1 };
150 this.ts
= Object
.keys(pairs
);
151 this.vs
= yf
.map
.call(pairs
, yf
.getter
, this.ts
);
154 var unlimited
= false;
155 yf
.each(function (t
) {
160 unlimited
= unlimited
|| (t
[t
.length
- 1] === "%");
162 this.minDuration
= a
- b
;
163 this.maxDuration
= (unlimited
|| a
=== b
)
166 var vMin
= Math
.min
.apply(Math
, this.vs
);
167 var vMax
= Math
.max
.apply(Math
, this.vs
);
168 this.constant
= vMin
=== vMax
&& this.vs
[0] * this.scale
;
172 schedule: function (param
, t0
, scale
, duration
) {
173 if (this.constant
!== false) {
174 param
.setValueAtTime(scale
* this.constant
, t0
);
176 yf
.each
.call(this, function (s
, v
) {
177 v
= v
* scale
* this.scale
;
178 var t
= t0
+ applyMod(s
, duration
);
180 param
.setValueAtTime(v
, t
);
182 param
.linearRampToValueAtTime(v
, t
);
183 }, this.ts
, this.vs
);
188 yuu
.AudioSample
= yuu
.Caching(yT({
189 constructor: function (path
, ctx
) {
190 ctx
= ctx
|| yuu
.audio
;
191 var url
= yuu
.resourcePath(path
, "sound", "wav");
193 this.ready
= yuu
.GET(url
, { responseType
: "arraybuffer" })
194 .then(ctx
.decodeAudioData
.bind(ctx
))
195 .then(yf
.setter
.bind(this, "buffer"))
198 }), function (args
) { return args
.length
<= 2 ? args
[0] : null; });
201 constructor: function (dfn
) {
202 this.envelope
= new yuu
.Envelope(dfn
.envelope
);
203 this.frequency
= dfn
.frequency
;
204 this.index
= dfn
.index
|| 1.0;
207 createModulator: function (ctx
, t0
, fundamental
, duration
) {
208 var modulator
= ctx
.createOscillator();
209 modulator
.frequency
.value
= applyMod(
210 this.frequency
, fundamental
);
212 modulator
.stop(t0
+ duration
);
213 var modulatorG
= ctx
.createGain();
214 modulator
.connect(modulatorG
);
215 this.envelope
.schedule(
216 modulatorG
.gain
, t0
, this.index
* fundamental
, duration
);
221 yuu
.Instrument
= yT({
222 constructor: function (dfn
) {
223 if (yf
.isString(dfn
)) {
224 var sampleName
= dfn
;
225 dfn
= { sample
: {} };
226 dfn
.sample
[sampleName
] = {};
228 this.envelope
= new yuu
.Envelope(dfn
.envelope
);
229 this.frequency
= dfn
.frequency
|| (dfn
.sample
? {} : { "x1": 1.0 });
230 this.modulator
= yf
.map(
231 yf
.new_(yuu
.Modulator
), yf
.arrayify(dfn
.modulator
|| []));
232 this.sample
= dfn
.sample
|| {};
233 this.ready
= yuu
.ready(
234 yf
.map(yf
.new_(yuu
.AudioSample
), Object
.keys(this.sample
)),
238 createSound: function (ctx
, t0
, fundamental
, amplitude
, duration
) {
239 // TODO: In the case of exactly one sample with a constant
240 // envelope, optimize out the extra gain node.
241 duration
= yf
.clamp(duration
|| 0,
242 this.envelope
.minDuration
,
243 this.envelope
.maxDuration
);
244 var ret
= ctx
.createGain();
247 yf
.ipairs(function (name
, params
) {
248 var buffer
= new yuu
.AudioSample(name
).buffer
;
249 if (buffer
&& !params
.loop
)
250 duration
= Math
.max(buffer
.duration
, duration
);
253 var modulators
= yf
.map(function (modulator
) {
254 return modulator
.createModulator(
255 ctx
, t0
, fundamental
, duration
);
258 yf
.ipairs
.call(this, function (name
, params
) {
259 var src
= ctx
.createBufferSource(name
);
260 src
.loop
= params
.loop
|| false;
261 src
.playbackRate
.value
= applyMod(
262 params
.playbackRate
|| 1, fundamental
|| ctx
.sampleRate
);
263 yf
.each(function (mod
) { mod
.connect(src
.playbackRate
); },
266 src
.start(t0
, params
.offset
|| 0, params
.duration
);
268 src
.start(t0
, params
.offset
|| 0);
269 src
.stop(t0
+ duration
);
273 yf
.ipairs
.call(this, function (mfreq
, mamp
) {
274 var osc
= ctx
.createOscillator();
275 osc
.frequency
.value
= applyMod(mfreq
, fundamental
);
277 osc
.stop(t0
+ duration
);
278 yf
.each(function (mod
) { mod
.connect(osc
.frequency
); },
281 var gain
= ctx
.createGain();
282 gain
.gain
.value
= mamp
;
291 this.envelope
.schedule(ret
.gain
, t0
, amplitude
, duration
);
297 return this.play(null, 0, 0, 1, 1);
299 function (ctx
, t
, freq
, amp
, duration
) {
300 ctx
= ctx
|| yuu
.audio
;
301 t
= t
|| ctx
.currentTime
;
302 var g
= this.createSound(ctx
, t
, freq
, amp
, duration
);
303 g
.connect(ctx
.destination
);
309 yuu
.Instruments
= yf
.mapValues(yf
.new_(yuu
.Instrument
), {
311 envelope
: { "0": 0, "0.016": 1, "-0.016": 1, "100%": 0 },
315 envelope
: { "0": 0, "0.016": 1, "-0.016": 1, "100%": 0 },
316 frequency
: { "x1": 0.83, "x1.5": 0.17 }
320 envelope
: { "0": 1 },
322 envelope
: { "0": 1 },
329 envelope
: { "0": 1, "2.5": 0.2, "5": 0 },
331 envelope
: { "0": 1, "2.5": 0.2, "5": 0 },
337 envelope
: { "0": 0, "0.2": 1, "0.4": 0.6, "-0.1": 0.5, "100%": 0 },
339 envelope
: { "0": 0, "0.2": 1, "0.4": 0.6,
340 "-0.1": 0.5, "100%": 0 },
347 // Tune to A440 by default, although every interface should provide
348 // some ways to work around this. This gives C4 = ~261.63 Hz.
349 // https://en.wikipedia.org/wiki/Scientific_pitch_notation
350 yuu
.C4_HZ
= 440 * Math
.pow(2, -9/12);
353 constructor: function (intervals
) {
354 this.intervals
= intervals
;
355 this.length
= this.intervals
.length
;
356 this.span
= yf
.foldl(function (a
, b
) { return a
+ b
; }, intervals
);
359 hz: function (tonic
, degree
, accidental
) {
360 accidental
= accidental
|| 0;
361 var s
= this.span
* ((degree
/ this.intervals
.length
) | 0)
363 degree
%= this.intervals
.length
;
365 degree
+= this.intervals
.length
;
369 while (degree
>= 1) {
371 s
+= this.intervals
[i
];
375 s
+= this.intervals
[i
] * degree
;
376 return tonic
* Math
.pow(2, s
/ 1200.0);
380 yuu
.Scales
= yf
.mapValues(yf
.new_(yuu
.Scale
), {
381 CHROMATIC
: yf
.repeat(100, 12),
382 MINOR
: [200, 100, 200, 200, 100, 200, 200],
383 MAJOR
: [200, 200, 100, 200, 200, 200, 100],
384 WHOLE_TONE
: [200, 200, 200, 200, 200, 200],
385 AUGMENTED
: [300, 100, 300, 100, 300, 100],
386 _17ET
: yf
.repeat(1200 / 17, 17),
387 DOUBLE_HARMONIC
: [100, 300, 100, 200, 100, 300, 100],
390 var DURATION
= { T
: 1/8, S: 1/4, I
: 1/2, Q
: 1, H
: 2, W
: 4,
391 ".": 1.5, "/": 1/3, "<": -1 };
392 var ACCIDENTAL
= { b
: -1, "#": 1, t
: 0.5, d
: -0.5 };
394 var NOTE
= /([TSIQHW][<.\/]*)?(?:([XZ]|(?:[A-G][b#dt]*[0-9]+))|([+-]?[0-9.]+)([b#dt]*))|([-+<>{]|(?:[TSIQHW][.\/]?))/g;
396 var LETTERS
= { Z
: null, X
: null };
398 yuu
.parseNote = function (note
, scale
, C4
) {
399 return (C4
|| yuu
.C4_HZ
) * Math
.pow(2, LETTERS
[note
] / 12);
402 yuu
.parseScore = function (score
, scale
, tonic
, C4
) {
405 // To play a scientific pitch note and advance the time, just
406 // use its name: G4, Cb2, A#0
408 // To adjust the length of the note, use T, S, I, Q (default),
409 // H, W for 32nd through whole. Append . to do
410 // time-and-a-half. Append / to cut into a third. Append < to
413 // To play a note on the provided scale, use a 0-based number
414 // (which can be negative). To move the current scale up or
415 // down, use + or -. For example, in C major, 0 and C4 produce
416 // the same note; after a -, 0 and C3 produce the same note.
418 // To rest, use Z or X.
420 // To play multiple notes at the same time, enclose them all with
421 // < ... >. The time will advance in accordance with the shortest
424 // To reset the time, scale offset, and duration, use a {.
425 // This can be more convenient when writing pieces with
426 // multiple parts than grouping, e.g.
427 // H < 1 8 > < 2 7 > < 3 6 > < 4 5 >
428 // is easier to understand when split into multiple lines:
432 scale
= scale
|| yuu
.Scales
.MAJOR
;
433 C4
= C4
|| yuu
.C4_HZ
;
434 tonic
= tonic
|| scale
.tonic
|| C4
;
435 if (yf
.isString(tonic
))
436 tonic
= yuu
.parseNote(tonic
, C4
);
442 var defaultDuration
= "Q";
445 function calcDuration (d
, m
) { return d
* DURATION
[m
]; }
446 function calcAccidental (d
, m
) { return d
* ACCIDENTAL
[m
]; }
448 while ((match
= NOTE
.exec(score
))) {
451 groupLength
= Infinity
;
454 t
+= groupLength
=== Infinity
? 0 : groupLength
;
458 degree
+= scale
.length
;
461 degree
-= scale
.length
;
467 defaultDuration
= "Q";
471 defaultDuration
= match
[5];
474 var letter
= match
[2];
475 var duration
= yf
.foldl(
476 calcDuration
, match
[1] || defaultDuration
, 1);
477 if (LETTERS
[letter
] !== null) {
478 var offset
= match
[3];
479 var accidental
= yf
.foldl(
480 calcAccidental
, match
[4] || "", 0) * 100;
485 ? C4
* Math
.pow(2, LETTERS
[letter
]/12.0)
486 : scale
.hz(tonic
, degree
+ (+offset
|| 0), accidental
)
489 if (groupLength
&& duration
> 0)
490 groupLength
= Math
.min(groupLength
, duration
);
496 notes
.sort(function (a
, b
) { return a
.time
- b
.time
; });
500 yf
.irange
.call(LETTERS
, function (i
) {
501 yf
.ipairs
.call(this, function (l
, o
) {
502 var b
= o
+ 12 * (i
- 4);
504 yf
.ipairs
.call(this, function (s
, m
) {
505 this[l
+ s
+ i
] = b
+ m
;
507 }, { C
: 0, D
: 2, E
: 4, F
: 5, G
: 7, A
: 9, B
: 11 });
510 yuu
.registerInitHook(function () {
511 if (!window
.AudioContext
)
512 throw new Error("Web Audio isn't supported.");
513 yuu
.audio
= new yuu
.Audio();
514 yuu
.defaultCommands
.volume
= yuu
.propcmd(
516 "get/set the current master audio volume", "0...1");
517 yuu
.defaultCommands
.musicVolume
= yuu
.propcmd(
518 yuu
.audio
, "musicVolume",
519 "get/set the current music volume", "0...1");
520 yuu
.defaultCommands
.mute
= yuu
.propcmd(
521 yuu
.audio
, "mute", "mute or unmute audio");
524 }).call(typeof exports
=== "undefined" ? this : exports
,
525 typeof exports
=== "undefined"
526 ? this.yuu
: (module
.exports
= require('./core')));