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;
34 this._volume
= this._masterVolume
.gain
.value
;
37 destination
: { alias
: "_masterVolume", readonly
: true },
38 music
: { alias
: "_musicVolume", readonly
: true },
40 _readStorage: function () {
43 yf
.each
.call(this, function (prop
) {
44 this[prop
] = this._storage
.getObject(prop
, this[prop
]);
45 }, ["volume", "musicVolume", "mute"]);
48 _writeStorage
: yf
.debounce(function () {
51 yf
.each
.call(this, function (prop
) {
52 this._storage
.setObject(prop
, this[prop
]);
53 }, ["volume", "musicVolume", "mute"]);
57 get: function () { return this._storage
; },
65 get: function () { return this._mute
; },
68 this.volume
= this.volume
;
73 get: function () { return this._volume
; },
76 v
= this._mute
? 0 : v
;
77 this._masterVolume
.gain
.value
= v
;
83 get: function () { return this._musicVolume
.gain
.value
; },
85 this._musicVolume
.gain
.value
= v
;
90 currentTime
: { alias
: "_ctx.currentTime" },
92 decodeAudioData: function (data
, hint
) {
95 return ctx
.decodeAudioData(data
);
97 return new Promise(function (resolve
, reject
) {
98 ctx
.decodeAudioData(data
, function (buffer
) {
101 reject(new Error("Error decoding audio buffer"
102 + (hint
? ": " + hint
: "")
103 + ": " + err
.toString()));
109 createBufferSource: function (path
) {
110 var source
= this._ctx
.createBufferSource();
111 var sample
= yf
.isString(path
)
112 ? new yuu
.AudioSample(path
, this)
114 if ((source
.buffer
= sample
.buffer
) === null) {
115 sample
.ready
.then(function () {
116 source
.buffer
= sample
.buffer
;
122 sampleRate
: { alias
: "_ctx.sampleRate" },
123 createGain
: { proxy
: "_ctx.createGain" },
124 createOscillator
: { proxy
: "_ctx.createOscillator" },
127 // FIXME: This parsing is garbagey, would be better to parse when
128 // first handed a dfn and turn everything into a function.
129 function applyMod (s
, v
) {
130 if (yf
.isFunction(s
))
134 else if (s
[0] === "-" || s
[0] === "+")
136 else if (s
[0] === "x" || s
[0] === "*")
137 return v
* +s
.substring(1);
138 else if (s
[s
.length
- 1] === "%")
139 return v
* (parseFloat(s
) / 100);
144 var Envelope
= yuu
.Envelope
= yT({
145 constructor: function (timeline
) {
146 timeline
= timeline
|| { 0: 1 };
147 this.ts
= Object
.keys(timeline
);
148 this.vs
= yf
.map
.call(timeline
, yf
.getter
, this.ts
);
149 var ts
= this.ts
.filter(isFinite
);
150 this.duration
= (Math
.max
.apply(Math
, ts
)
151 - Math
.min
.apply(Math
, ts
));
152 this.unlimited
= !this.duration
|| ts
.length
!== this.ts
.length
;
155 clampDuration: function (duration
) {
156 return this.unlimited
157 ? Math
.max(duration
, this.duration
)
161 schedule: function (param
, t0
, scale
, duration
) {
162 yf
.each(function (s
, v
) {
164 var t
= t0
+ applyMod(s
, duration
);
166 param
.setValueAtTime(v
, t
);
168 param
.linearRampToValueAtTime(v
, t
);
169 }, this.ts
, this.vs
);
173 yuu
.AudioSample
= yuu
.Caching(yT({
174 constructor: function (path
, ctx
) {
175 ctx
= ctx
|| yuu
.audio
;
176 var url
= yuu
.resourcePath(path
, "sound", "wav");
178 this.ready
= yuu
.GET(url
, { responseType
: "arraybuffer" })
179 .then(function (data
) {
180 return ctx
.decodeAudioData(data
, url
);
182 .then(yf
.setter
.bind(this, "buffer"))
185 }), function (args
) { return args
.length
<= 2 ? args
[0] : null; });
188 constructor: function (dfn
) {
189 this.envelope
= new Envelope(dfn
.envelope
);
190 this.frequency
= dfn
.frequency
;
191 this.index
= dfn
.index
|| 1.0;
194 createModulator: function (ctx
, t0
, fundamental
, duration
) {
195 var modulator
= ctx
.createOscillator();
196 modulator
.frequency
.value
= applyMod(
197 this.frequency
, fundamental
);
199 modulator
.stop(t0
+ duration
);
200 var modulatorG
= ctx
.createGain();
201 modulator
.connect(modulatorG
);
202 this.envelope
.schedule(
203 modulatorG
.gain
, t0
, this.index
* fundamental
, duration
);
208 var BaseInstrument
= yT({
211 return this.play(null, 0, 0, 1, 1);
213 function (ctx
, t
, freq
, amp
, duration
) {
214 ctx
= ctx
|| yuu
.audio
;
215 t
= t
|| ctx
.currentTime
;
216 var g
= this.createSound(ctx
, t
, freq
, amp
, duration
);
217 g
.connect(ctx
.destination
);
223 var FastInstrument
= yT(BaseInstrument
, {
224 constructor: function (dfn
) {
225 this.sample
= new yuu
.AudioSample(dfn
);
226 this.ready
= this.sample
.ready
.then(yf
.K(this));
229 createSound: function (ctx
, t0
, fundamental
, amplitude
, duration
) {
230 var src
= ctx
.createBufferSource(this.sample
);
233 src
.stop(t0
+ duration
);
234 if (amplitude
!== 1.0) {
235 ret
= ctx
.createGain();
237 ret
.gain
.value
= amplitude
;
243 var Instrument
= yT(BaseInstrument
, {
244 constructor: function (dfn
) {
245 this.envelope
= new yuu
.Envelope(dfn
.envelope
);
246 this.frequency
= dfn
.frequency
|| (dfn
.sample
? {} : { "x1": 1.0 });
247 this.modulator
= yf
.map(
248 yf
.new_(yuu
.Modulator
), yf
.arrayify(dfn
.modulator
|| []));
249 this.sample
= dfn
.sample
|| {};
250 this.ready
= yuu
.ready(
251 yf
.map(yf
.new_(yuu
.AudioSample
), Object
.keys(this.sample
)),
255 createSound: function (ctx
, t0
, fundamental
, amplitude
, duration
) {
256 duration
= this.envelope
.clampDuration(duration
|| 0);
257 var ret
= ctx
.createGain();
260 yf
.ipairs(function (name
, params
) {
261 var buffer
= new yuu
.AudioSample(name
).buffer
;
262 if (buffer
&& !params
.loop
)
263 duration
= Math
.max(buffer
.duration
, duration
);
266 var modulators
= yf
.map(function (modulator
) {
267 return modulator
.createModulator(
268 ctx
, t0
, fundamental
, duration
);
271 yf
.ipairs(function (name
, params
) {
272 var src
= ctx
.createBufferSource(name
);
273 src
.loop
= params
.loop
|| false;
274 src
.playbackRate
.value
= applyMod(
275 params
.playbackRate
|| 1, fundamental
|| ctx
.sampleRate
);
276 yf
.each(function (mod
) { mod
.connect(src
.playbackRate
); },
279 src
.start(t0
, params
.offset
|| 0, params
.duration
);
281 src
.start(t0
, params
.offset
|| 0);
282 src
.stop(t0
+ duration
);
286 yf
.ipairs(function (mfreq
, mamp
) {
287 var osc
= ctx
.createOscillator();
288 osc
.frequency
.value
= applyMod(mfreq
, fundamental
);
290 osc
.stop(t0
+ duration
);
291 yf
.each(function (mod
) { mod
.connect(osc
.frequency
); },
294 var gain
= ctx
.createGain();
295 gain
.gain
.value
= mamp
;
304 this.envelope
.schedule(ret
.gain
, t0
, amplitude
, duration
);
309 yuu
.Instrument = function (dfn
) {
310 return yf
.isString(dfn
)
311 ? new FastInstrument(dfn
)
312 : new Instrument(dfn
);
315 yuu
.Instruments
= yf
.mapValues(yf
.new_(yuu
.Instrument
), {
317 envelope
: { "0": 0, "0.016": 1, "-0.016": 1, "100%": 0 },
321 envelope
: { "0": 0, "0.016": 1, "-0.016": 1, "100%": 0 },
322 frequency
: { "x1": 0.83, "x1.5": 0.17 }
326 envelope
: { "0": 1 },
328 envelope
: { "0": 1 },
335 envelope
: { "0": 1, "2.5": 0.2, "5": 0 },
337 envelope
: { "0": 1, "2.5": 0.2, "5": 0 },
343 envelope
: { "0": 0, "0.2": 1, "0.4": 0.6, "-0.1": 0.5, "100%": 0 },
345 envelope
: { "0": 0, "0.2": 1, "0.4": 0.6,
346 "-0.1": 0.5, "100%": 0 },
353 // Tune to A440 by default, although every interface should provide
354 // some ways to work around this. This gives C4 = ~261.63 Hz.
355 // https://en.wikipedia.org/wiki/Scientific_pitch_notation
356 yuu
.C4_HZ
= 440 * Math
.pow(2, -9/12);
359 constructor: function (intervals
) {
360 this.intervals
= intervals
;
361 this.length
= this.intervals
.length
;
362 this.span
= yf
.foldl(function (a
, b
) { return a
+ b
; }, intervals
);
365 hz: function (tonic
, degree
, accidental
) {
366 accidental
= accidental
|| 0;
367 var s
= this.span
* ((degree
/ this.intervals
.length
) | 0)
369 degree
%= this.intervals
.length
;
371 degree
+= this.intervals
.length
;
375 while (degree
>= 1) {
377 s
+= this.intervals
[i
];
381 s
+= this.intervals
[i
] * degree
;
382 return tonic
* Math
.pow(2, s
/ 1200.0);
386 yuu
.Scales
= yf
.mapValues(yf
.new_(yuu
.Scale
), {
387 CHROMATIC
: yf
.repeat(100, 12),
388 MINOR
: [200, 100, 200, 200, 100, 200, 200],
389 MAJOR
: [200, 200, 100, 200, 200, 200, 100],
390 WHOLE_TONE
: [200, 200, 200, 200, 200, 200],
391 AUGMENTED
: [300, 100, 300, 100, 300, 100],
392 _17ET
: yf
.repeat(1200 / 17, 17),
393 DOUBLE_HARMONIC
: [100, 300, 100, 200, 100, 300, 100],
396 var DURATION
= { T
: 1/8, S: 1/4, I
: 1/2, Q
: 1, H
: 2, W
: 4,
397 ".": 1.5, "/": 1/3, "<": -1 };
398 var ACCIDENTAL
= { b
: -1, "#": 1, t
: 0.5, d
: -0.5 };
400 var NOTE
= /([TSIQHW][<.\/]*)?(?:([XZ]|(?:[A-G][b#dt]*[0-9]+))|([+-]?[0-9.]+)([b#dt]*))|([-+<>{]|(?:[TSIQHW][.\/]?))/g;
402 var LETTERS
= { Z
: null, X
: null };
404 yuu
.parseNote = function (note
, scale
, C4
) {
405 return (C4
|| yuu
.C4_HZ
) * Math
.pow(2, LETTERS
[note
] / 12);
408 yuu
.parseScore = function (score
, scale
, tonic
, C4
) {
411 // To play a scientific pitch note and advance the time, just
412 // use its name: G4, Cb2, A#0
414 // To adjust the length of the note, use T, S, I, Q (default),
415 // H, W for 32nd through whole. Append . to do
416 // time-and-a-half. Append / to cut into a third. Append < to
419 // To play a note on the provided scale, use a 0-based number
420 // (which can be negative). To move the current scale up or
421 // down, use + or -. For example, in C major, 0 and C4 produce
422 // the same note; after a -, 0 and C3 produce the same note.
424 // To rest, use Z or X.
426 // To play multiple notes at the same time, enclose them all with
427 // < ... >. The time will advance in accordance with the shortest
430 // To reset the time, scale offset, and duration, use a {.
431 // This can be more convenient when writing pieces with
432 // multiple parts than grouping, e.g.
433 // H < 1 8 > < 2 7 > < 3 6 > < 4 5 >
434 // is easier to understand when split into multiple lines:
438 scale
= scale
|| yuu
.Scales
.MAJOR
;
439 C4
= C4
|| yuu
.C4_HZ
;
440 tonic
= tonic
|| scale
.tonic
|| C4
;
441 if (yf
.isString(tonic
))
442 tonic
= yuu
.parseNote(tonic
, C4
);
448 var defaultDuration
= "Q";
451 function calcDuration (d
, m
) { return d
* DURATION
[m
]; }
452 function calcAccidental (d
, m
) { return d
* ACCIDENTAL
[m
]; }
454 while ((match
= NOTE
.exec(score
))) {
457 groupLength
= Infinity
;
460 t
+= groupLength
=== Infinity
? 0 : groupLength
;
464 degree
+= scale
.length
;
467 degree
-= scale
.length
;
473 defaultDuration
= "Q";
477 defaultDuration
= match
[5];
480 var letter
= match
[2];
481 var duration
= yf
.foldl(
482 calcDuration
, match
[1] || defaultDuration
, 1);
483 if (LETTERS
[letter
] !== null) {
484 var offset
= match
[3];
485 var accidental
= yf
.foldl(
486 calcAccidental
, match
[4] || "", 0) * 100;
491 ? C4
* Math
.pow(2, LETTERS
[letter
]/12.0)
492 : scale
.hz(tonic
, degree
+ (+offset
|| 0), accidental
)
495 if (groupLength
&& duration
> 0)
496 groupLength
= Math
.min(groupLength
, duration
);
502 notes
.sort(function (a
, b
) { return a
.time
- b
.time
; });
506 yf
.irange
.call(LETTERS
, function (i
) {
507 yf
.ipairs
.call(this, function (l
, o
) {
508 var b
= o
+ 12 * (i
- 4);
510 yf
.ipairs
.call(this, function (s
, m
) {
511 this[l
+ s
+ i
] = b
+ m
;
513 }, { C
: 0, D
: 2, E
: 4, F
: 5, G
: 7, A
: 9, B
: 11 });
516 yuu
.registerInitHook(function () {
517 if (!window
.AudioContext
)
518 throw new Error("Web Audio isn't supported.");
519 yuu
.audio
= new yuu
.Audio();
520 yuu
.defaultCommands
.volume
= yuu
.propcmd(
522 "get/set the current master audio volume", "0...1");
523 yuu
.defaultCommands
.musicVolume
= yuu
.propcmd(
524 yuu
.audio
, "musicVolume",
525 "get/set the current music volume", "0...1");
526 yuu
.defaultCommands
.mute
= yuu
.propcmd(
527 yuu
.audio
, "mute", "mute or unmute audio");
530 }).call(typeof exports
=== "undefined" ? this : exports
,
531 typeof exports
=== "undefined"
532 ? this.yuu
: (module
.exports
= require('./core')));