UNPKG

picoaudio

Version:

PicoAudio.js is light MIDI player using Web Audio API

1,647 lines (1,374 loc) 128 kB
var PicoAudio = (function () { 'use strict'; function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function (obj) { return typeof obj; }; } else { _typeof = function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); } function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } } function _construct(Parent, args, Class) { if (_isNativeReflectConstruct()) { _construct = Reflect.construct; } else { _construct = function _construct(Parent, args, Class) { var a = [null]; a.push.apply(a, args); var Constructor = Function.bind.apply(Parent, a); var instance = new Constructor(); if (Class) _setPrototypeOf(instance, Class.prototype); return instance; }; } return _construct.apply(null, arguments); } function _isNativeFunction(fn) { return Function.toString.call(fn).indexOf("[native code]") !== -1; } function _wrapNativeSuper(Class) { var _cache = typeof Map === "function" ? new Map() : undefined; _wrapNativeSuper = function _wrapNativeSuper(Class) { if (Class === null || !_isNativeFunction(Class)) return Class; if (typeof Class !== "function") { throw new TypeError("Super expression must either be null or a function"); } if (typeof _cache !== "undefined") { if (_cache.has(Class)) return _cache.get(Class); _cache.set(Class, Wrapper); } function Wrapper() { return _construct(Class, arguments, _getPrototypeOf(this).constructor); } Wrapper.prototype = Object.create(Class.prototype, { constructor: { value: Wrapper, enumerable: false, writable: true, configurable: true } }); return _setPrototypeOf(Wrapper, Class); }; return _wrapNativeSuper(Class); } function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } function _possibleConstructorReturn(self, call) { if (call && (typeof call === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); } function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; } /* argsObj { debug, audioContext, picoAudio, etc (this.settings.xxx) } */ function picoAudioConstructor(argsObj) { this.debug = false; this.isStarted = false; this.isPlayed = false; this.settings = { masterVolume: 1, generateVolume: 0.15, tempo: 120, basePitch: 440, resolution: 480, isWebMIDI: false, WebMIDIPortOutputs: null, WebMIDIPortOutput: null, WebMIDIPort: -1, // -1:auto WebMIDIPortSysEx: true, // MIDIデバイスのフルコントロールをするかどうか(SysExを使うかどうか)(httpsじゃないと使えない) isReverb: true, // リバーブONにするか reverbVolume: 1.5, initReverb: 10, isChorus: true, chorusVolume: 0.5, isCC111: true, loop: false, isSkipBeginning: false, // 冒頭の余白をスキップ isSkipEnding: true, // 末尾の空白をスキップ holdOnValue: 64, maxPoly: -1, // 同時発音数 -1:infinity maxPercPoly: -1, // 同時発音数(パーカッション) -1:infinity isOfflineRendering: false, // TODO 演奏データを作成してから演奏する isSameDrumSoundOverlap: false, // 同じドラムの音が重なることを許容するか baseLatency: -1 // レイテンシの設定 -1:auto }; // argsObjで設定値が指定されていたら上書きする rewriteVar(this, argsObj, "debug"); for (var key in this.settings) { rewriteVar(this.settings, argsObj, key); } this.events = []; this.trigger = { isNoteTrigger: true, play: function play() {}, stop: function stop() {}, noteOn: function noteOn() {}, noteOff: function noteOff() {}, songEnd: function songEnd() {} }; this.states = { isPlaying: false, startTime: 0, stopTime: 0, stopFuncs: [], webMIDIWaitState: null, webMIDIStopTime: 0, playIndices: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], updateBufTime: 100, updateBufMaxTime: 350, updateIntervalTime: 0, latencyLimitTime: 0 }; this.hashedDataList = []; this.hashedMessageList = []; this.playData = null; this.channels = []; this.tempoTrack = [{ timing: 0, value: 120 }, { timing: 0, value: 120 }]; this.cc111Time = -1; this.onSongEndListener = null; this.baseLatency = 0.01; // チャンネルの設定値(音色, 減衰, 音量) // for (var i = 0; i < 17; i++) { this.channels.push([0, 0, 1]); } // AudioContextがある場合はそのまま初期化、なければAudioContextを用いる初期化をinit()で if (argsObj && argsObj.audioContext) { this.init(argsObj); } } function rewriteVar(dist, src, vr) { if (src && src[vr] != null && dist && dist[vr] != null) { dist[vr] = src[vr]; } } /** * 固定パターンの乱数を提供するクラス */ var RandomUtil = /*#__PURE__*/function () { function RandomUtil() { _classCallCheck(this, RandomUtil); } _createClass(RandomUtil, null, [{ key: "resetSeed", /** * 乱数のシード値をリセットする */ value: function resetSeed() { this.init = true; this.x = 123456789; this.y = 362436069; this.z = 521288629; this.w = 8867512; } /** * 乱数を返す * * Math.random() と違い、毎回固定パターンで乱数が返される * Xorshiftアルゴリズム * @returns {number} 乱数 */ }, { key: "random", value: function random() { if (!this.init) this.resetSeed(); var t = this.x ^ this.x << 11; this.x = this.y; this.y = this.z; this.z = this.w; var r = this.w = this.w ^ this.w >>> 19 ^ (t ^ t >>> 8); r = Math.abs(r) / 2147483648 % 2; return r; } }]); return RandomUtil; }(); /** * 補間を提供するクラス */ var InterpolationUtil = /*#__PURE__*/function () { function InterpolationUtil() { _classCallCheck(this, InterpolationUtil); } _createClass(InterpolationUtil, null, [{ key: "lerpWave", /** * 波形を線形補間する * @param {AudioBuffer} buffer 補間結果を入れるAudioBuffer * @param {Array} vtBufs 仮想音源の配列([Float32Array, Float32Array]) */ value: function lerpWave(buffer, vtBufs) { // 仮想サンプルレート音源を本番音源に変換する // var bufferSize = buffer.getChannelData(0).length; var vtBufsSize = vtBufs[0].length; if (bufferSize == vtBufsSize) { // 線形補間の必要なし // for (var ch = 0; ch < 2; ch++) { var data = buffer.getChannelData(ch); var vtBuf = vtBufs[ch]; for (var i = 0; i < bufferSize; i++) { data[i] = vtBuf[i]; } } } else { // 線形補間 // var ratio = vtBufsSize / bufferSize; for (var _ch = 0; _ch < 2; _ch++) { var _data = buffer.getChannelData(_ch); var _vtBuf = vtBufs[_ch]; for (var _i = 0; _i < bufferSize; _i++) { // 線形補間しながら波形を作成 // // TODO 音がまだ少し違和感あるので、スプライン補正に変更した方がいいかも // var idxF = _i * ratio; var idx1 = Math.trunc(idxF); var idx2 = (idx1 + 1) % vtBufsSize; var idxR = idxF - idx1; var w = _vtBuf[idx1] * (1 - idxR) + _vtBuf[idx2] * idxR; _data[_i] = w; } } } } }]); return InterpolationUtil; }(); function init(argsObj) { if (this.isStarted) return; this.isStarted = true; var audioContext = argsObj && argsObj.audioContext; var picoAudio = argsObj && argsObj.picoAudio; // AudioContextを生成 // var AudioContext = window.AudioContext || window.webkitAudioContext; this.context = audioContext ? audioContext : new AudioContext(); // マスターボリューム // // リアルタイムで音量変更するためにdestination前にgainNodeを一つ噛ませる this.masterGainNode = this.context.createGain(); this.masterGainNode.gain.value = this.settings.masterVolume; // 仮想サンプルレート // var sampleRate = this.context.sampleRate; var sampleRateVT = sampleRate >= 48000 ? 48000 : sampleRate; // ホワイトノイズ // if (picoAudio && picoAudio.whitenoise) { // 使いまわし this.whitenoise = picoAudio.whitenoise; } else { RandomUtil.resetSeed(); // 乱数パターンを固定にする(Math.random()を使わない) // 再生環境のサンプルレートによって音が変わってしまうので // // 一旦仮想サンプルレートで音源を作成する // var seLength = 1; var sampleLength = sampleRate * seLength; var sampleLengthVT = sampleRateVT * seLength; var vtBufs = []; for (var ch = 0; ch < 2; ch++) { vtBufs.push(new Float32Array(sampleLengthVT)); var vtBuf = vtBufs[ch]; for (var i = 0; i < sampleLengthVT; i++) { var r = RandomUtil.random(); vtBuf[i] = r * 2 - 1; } } // 仮想サンプルレート音源を本番音源に変換する // this.whitenoise = this.context.createBuffer(2, sampleLength, sampleRate); InterpolationUtil.lerpWave(this.whitenoise, vtBufs); } // リバーブ用のインパルス応答音声データ作成(てきとう) // if (picoAudio && picoAudio.impulseResponse) { // 使いまわし this.impulseResponse = picoAudio.impulseResponse; } else { RandomUtil.resetSeed(); // 乱数パターンを固定にする(Math.random()を使わない) // 再生環境のサンプルレートによって音が変わってしまうので // // 一旦仮想サンプルレートで音源を作成する // var _seLength = 3.5; var _sampleLength = sampleRate * _seLength; var _sampleLengthVT = sampleRateVT * _seLength; var _vtBufs = []; for (var _ch = 0; _ch < 2; _ch++) { _vtBufs.push(new Float32Array(_sampleLengthVT)); var _vtBuf = _vtBufs[_ch]; for (var _i = 0; _i < _sampleLengthVT; _i++) { var v = (_sampleLengthVT - _i) / _sampleLengthVT; var s = _i / sampleRateVT; var d = (s < 0.030 ? 0 : v) * (s >= 0.030 && s < 0.031 ? v * 2 : v) * (s >= 0.040 && s < 0.042 ? v * 1.5 : v) * (s >= 0.050 && s < 0.054 ? v * 1.25 : v) * RandomUtil.random() * 0.2 * Math.pow(v - 0.030, 4); _vtBuf[_i] = d; } } // 仮想サンプルレート音源を本番音源に変換する // this.impulseResponse = this.context.createBuffer(2, _sampleLength, this.context.sampleRate); InterpolationUtil.lerpWave(this.impulseResponse, _vtBufs); } // リバーブ用のAudioNode作成・接続 // this.convolver = this.context.createConvolver(); this.convolver.buffer = this.impulseResponse; this.convolver.normalize = true; this.convolverGainNode = this.context.createGain(); this.convolverGainNode.gain.value = this.settings.reverbVolume; this.convolver.connect(this.convolverGainNode); this.convolverGainNode.connect(this.masterGainNode); this.masterGainNode.connect(this.context.destination); // コーラス用のAudioNode作成・接続 // this.chorusDelayNode = this.context.createDelay(); this.chorusGainNode = this.context.createGain(); this.chorusOscillator = this.context.createOscillator(); this.chorusLfoGainNode = this.context.createGain(); this.chorusDelayNode.delayTime.value = 0.025; this.chorusLfoGainNode.gain.value = 0.010; this.chorusOscillator.frequency.value = 0.05; this.chorusGainNode.gain.value = this.settings.chorusVolume; this.chorusOscillator.connect(this.chorusLfoGainNode); this.chorusLfoGainNode.connect(this.chorusDelayNode.delayTime); this.chorusDelayNode.connect(this.chorusGainNode); this.chorusGainNode.connect(this.masterGainNode); this.masterGainNode.connect(this.context.destination); this.chorusOscillator.start(0); // レイテンシの設定 // this.baseLatency = this.context.baseLatency || this.baseLatency; if (this.settings.baseLatency != -1) { this.baseLatency = this.settings.baseLatency; } } var Performance = /*#__PURE__*/function () { function Performance() { _classCallCheck(this, Performance); } _createClass(Performance, null, [{ key: "now", value: function now() { // Unsupport performance.now() if (this._now == null) { if (typeof window.performance === "undefined") { this._now = function () { return window.Date.now(); }; } else { this._now = function () { return window.performance.now(); }; } } return this._now(); } }]); return Performance; }(); var Number_MAX_SAFE_INTEGER = 0x1FFFFFFFFFFFFF; function setData(data) { if (this.debug) { var syoriTimeS = Performance.now(); } if (this.states.isPlaying) this.stop(); this.playData = data; this.settings.resolution = data.header.resolution; this.settings.tempo = data.tempo || 120; this.tempoTrack = data.tempoTrack; this.cc111Time = data.cc111Time; this.firstNoteOnTiming = data.firstNoteOnTiming; this.lastNoteOffTiming = data.lastNoteOffTiming; this.firstNoteOnTime = data.firstNoteOnTime; this.lastNoteOffTime = data.lastNoteOffTime; this.lastEventTiming = data.lastEventTiming; this.lastEventTime = data.lastEventTime; this.initStatus(); if (this.debug) { var syoriTimeE = Performance.now(); console.log("setData time", syoriTimeE - syoriTimeS); } return this; } function initStatus(isSongLooping, isLight) { // WebMIDI使用中の場合、initStatus()連打の対策 // if (this.settings.isWebMIDI) { if (this.states.webMIDIWaitState != null) return; } // 演奏中の場合、停止する // this.stop(isSongLooping); // statesを初期化 // this.states = { isPlaying: false, startTime: 0, stopTime: 0, stopFuncs: [], webMIDIWaitState: null, webMIDIStopTime: this.states.webMIDIStopTime, playIndices: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], updateBufTime: this.states.updateBufTime, updateBufMaxTime: this.states.updateBufMaxTime, updateIntervalTime: this.states.updateIntervalTime, latencyLimitTime: this.states.latencyLimitTime, noteOnAry: [], noteOffAry: [] }; // WebMIDIの初期化・リセットメッセージ送信 // if (this.settings.isWebMIDI && !isLight) { if (isSongLooping) return; if (this.settings.WebMIDIPortOutput == null) { this.startWebMIDI(); return; } if (this.settings.WebMIDIPortSysEx) { // GM1システム・オン this.settings.WebMIDIPortOutput.send([0xF0, 0x7E, 0x7F, 0x09, 0x01, 0xF7]); } else { // SysExの使用が拒否されているので、できる限り設定値を初期値に戻す for (var t = 0; t < 16; t++) { this.settings.WebMIDIPortOutput.send([0xC0 + t, 0]); this.settings.WebMIDIPortOutput.send([0xE0 + t, 0, 64]); // ピッチあたりのずれがひどくなる場合がある よくわからない this.settings.WebMIDIPortOutput.send([0xB0 + t, 100, 0]); this.settings.WebMIDIPortOutput.send([0xB0 + t, 101, 0]); this.settings.WebMIDIPortOutput.send([0xB0 + t, 6, 2]); //pitchbend this.settings.WebMIDIPortOutput.send([0xB0 + t, 100, 1]); this.settings.WebMIDIPortOutput.send([0xB0 + t, 96, 0]); this.settings.WebMIDIPortOutput.send([0xB0 + t, 97, 64]); //tuning? this.settings.WebMIDIPortOutput.send([0xB0 + t, 7, 100]); // volume this.settings.WebMIDIPortOutput.send([0xB0 + t, 10, 64]); // pan this.settings.WebMIDIPortOutput.send([0xB0 + t, 11, 127]); // expression //this.settings.WebMIDIPortOutput.send([0xB0+t, 91, 40]); // リバーブ以外のエフェクトに設定される場合がありそうなのでコメントアウト //this.settings.WebMIDIPortOutput.send([0xB0+t, 93, 0]); // コーラス以外のエフェクトに設定されるのか音が出なくなる場合があるのでコメントアウト this.settings.WebMIDIPortOutput.send([0xB0 + t, 98, 0]); this.settings.WebMIDIPortOutput.send([0xB0 + t, 99, 0]); //this.settings.WebMIDIPortOutput.send([0xB0+t, 121, 0]); this.settings.WebMIDIPortOutput.send([0xB0 + t, 122, 0]); } } } } var ArrayUtil = /*#__PURE__*/function (_Array) { _inherits(ArrayUtil, _Array); var _super = _createSuper(ArrayUtil); function ArrayUtil() { _classCallCheck(this, ArrayUtil); return _super.apply(this, arguments); } _createClass(ArrayUtil, null, [{ key: "delete", /** * 配列から要素1つを削除する * * Array.splice(index, 1); を高速化する * 特に配列末尾、又は配列先頭を削除するときに高速処理が期待できる * @param {Array} array 配列 * @param {number} index 添え字 */ value: function _delete(array, index) { if (index == array.length - 1) array.pop(); // 配列末尾をArray.pop()で削除すると高速化する else if (index == 0) array.shift(); // 配列先頭をArray.shift()で削除すると高速化する(あまり変わらない環境もある) else array.splice(index, 1); // 配列先頭・末尾以外を削除する場合はArray.splice()で削除する } }]); return ArrayUtil; }( /*#__PURE__*/_wrapNativeSuper(Array)); var ParseUtil = /*#__PURE__*/function () { function ParseUtil() { _classCallCheck(this, ParseUtil); } _createClass(ParseUtil, null, [{ key: "getInt", /** * バイト配列内に含まれる"データ長"を数値に変換する * @param {Uint8Array} arr バイト配列 * @param {number} startIdx データ長の始点の場所(index) * @param {number} endIdx データ長の終点の場所(index) - 1 * @returns {number} データ長 */ value: function getInt(arr, startIdx, endIdx) { var value = 0; for (var i = startIdx; i < endIdx; i++) { value = (value << 8) + arr[i]; } return value; } /** * バイト配列内に含まれる"可変長のデータ長"を数値に変換する * @param {Uint8Array} arr バイト配列 * @param {number} startIdx データ長の始点の場所(index) * @param {number} endIdx データ長の終点の場所(index) - 1 (終点の場所は多くてもかまわない) * @returns {Array} [データ長, "可変長のデータ長"のバイト数] */ }, { key: "variableLengthToInt", value: function variableLengthToInt(arr, startIdx, endIdx) { var i = startIdx; var value = 0; while (i < endIdx - 1 && arr[i] >= 0x80) { if (i < startIdx + 4) value = (value << 7) + (arr[i] - 0x80); i++; } value = (value << 7) + arr[i]; i++; return [value, i - startIdx]; } /** * デルタタイムの順番になるように配列に挿入 * @param {PicoAudio} that PicoAudioインスタンス * @param {number} ch チャンネル番号 * @param {number} time デルタタイム * @param {number} p 対象のMIDIイベントの場所(SMFデータ内の位置) * @param {number} len MIDIイベントの長さ */ }, { key: "chIndicesInsert", value: function chIndicesInsert(that, ch, time, p, len) { var indices = ch.indices; // デルタタイムの順番になるようにリスト配列に挿入 // if (ch.indicesLength >= 4 && time < indices[ch.indicesFoot]) { // Insert // while (ch.indicesCur != -1) { if (time < indices[ch.indicesCur]) { if (ch.indicesCur == ch.indicesHead) { ch.indicesHead = ch.indicesLength; } else { indices[ch.indicesPre + 3] = ch.indicesLength; } indices[ch.indicesLength] = time; indices[ch.indicesLength + 1] = len; indices[ch.indicesLength + 2] = p; indices[ch.indicesLength + 3] = ch.indicesCur; ch.indicesPre = ch.indicesLength; ch.indicesLength += 4; break; } ch.indicesPre = ch.indicesCur; ch.indicesCur = indices[ch.indicesCur + 3]; } } else { // Push // if (ch.indicesLength >= 4) { indices[ch.indicesFoot + 3] = ch.indicesLength; } else { ch.indicesHead = 0; } ch.indicesFoot = ch.indicesLength; indices[ch.indicesLength] = time; indices[ch.indicesLength + 1] = len; indices[ch.indicesLength + 2] = p; indices[ch.indicesLength + 3] = -1; ch.indicesLength += 4; } } }]); return ParseUtil; }(); var UpdateNote = /*#__PURE__*/function () { function UpdateNote() { _classCallCheck(this, UpdateNote); } _createClass(UpdateNote, null, [{ key: "init", /** * 1ms毎処理用の変数を初期化 */ value: function init(picoAudio, currentTime) { this.updatePreTime = Performance.now(); this.pPreTime = Performance.now(); this.cPreTime = picoAudio.context.currentTime * 1000; this.pTimeSum = 0; this.cTimeSum = 0; this.cnt = 0; this.initCurrentTime = currentTime; } /** * 再生中、1ms毎に呼ばれるコールバック * (ブラウザの制限で実際は最短4ms毎に呼ばれる) * @returns {number} 現在の時間 */ }, { key: "update", value: function update(picoAudio) { var _this = this; var context = picoAudio.context; var settings = picoAudio.settings; var states = picoAudio.states; var baseLatency = picoAudio.baseLatency; var updateNowTime = Performance.now(); var updatePreTime = this.updatePreTime; var pPreTime = this.pPreTime; var cPreTime = this.cPreTime; var pTimeSum = this.pTimeSum; var cTimeSum = this.cTimeSum; var cnt = this.cnt; // サウンドが重くないか監視(フリーズ対策) // // performance.now()とAudioContext.currentTimeの時間差を計算し // AudioContext.currentTimeが遅れていたら処理落ちしていると判断する var updateBufTime = updateNowTime - updatePreTime; var pTime = updateNowTime; var cTime = context.currentTime * 1000; pTimeSum += pTime - pPreTime; cTimeSum += cTime - cPreTime; pPreTime = pTime; cPreTime = cTime; var latencyTime = pTimeSum - cTimeSum; states.latencyTime = latencyTime; // サウンドが重い場合、負荷軽減処理を発動するリミットを上げていく // if (latencyTime >= 100) { // currentTimeが遅い(サウンドが重い) states.latencyLimitTime += latencyTime; cTimeSum += 100; } else if (latencyTime <= -100) { // currentTimeが速い(誤差) cTimeSum = pTimeSum; } else { if (states.latencyLimitTime > 0) { // currentTimeが丁度いい states.latencyLimitTime -= updateBufTime * 0.003; if (states.latencyLimitTime < 0) states.latencyLimitTime = 0; } } // ノートを先読み度合いを自動調整(予約しすぎると重くなる) // states.updateIntervalTime = updateBufTime; if (states.updateBufTime < updateBufTime) { // 先読み遅れている場合 states.updateBufTime = updateBufTime; } else { // 先読み量に余裕がある場合 // 先読み量を少しずつ減らす // if (states.updateBufMaxTime > 350) { states.updateBufMaxTime -= states.updateBufMaxTime * 0.002; } // 先読み量を少しずつ増やす // if (states.updateBufTime < 20) { states.updateBufTime += states.updateBufTime * 0.0005; } if (states.updateBufMaxTime >= 10 && states.updateBufMaxTime < 340) { states.updateBufMaxTime += states.updateBufMaxTime * 0.002; } } // 先読み量が足りなくなった場合 if (states.updateBufTime > states.updateBufMaxTime) { if (updateBufTime >= 900 && states.latencyLimitTime <= 150) { // バックグラウンドっぽくて重くない場合、バックグラウンド再生 states.updateBufMaxTime += updateBufTime; } else { // 通常 var tempTime = updateBufTime - states.updateBufMaxTime; states.updateBufTime = states.updateBufMaxTime; // 先読み量が小さい場合大きくする if (states.updateBufMaxTime < 10) { states.updateBufTime = states.updateBufMaxTime; states.updateBufMaxTime *= 1.25; } else { states.updateBufMaxTime += tempTime / 2; } } if (states.updateBufMaxTime > 1100) states.updateBufMaxTime = 1100; } // サウンドが重すぎる場合、先読み度合いを小さくして負荷軽減 // if (states.latencyLimitTime > 150) { cTimeSum = pTimeSum; states.latencyLimitTime -= 5; if (states.latencyLimitTime > 1000) states.latencyLimitTime = 1000; // ノート先読みをかなり小さくする(フリーズ対策) states.updateBufMaxTime = 1; states.updateBufTime = 1; updateBufTime = 1; } // 再生処理 // for (var ch = 0; ch < 16; ch++) { var notes = picoAudio.playData.channels[ch].notes; var idx = states.playIndices[ch]; var _loop = function _loop() { var note = notes[idx]; var curTime = cnt == 0 ? _this.initCurrentTime - states.startTime : context.currentTime - states.startTime; // 終わったノートは演奏せずにスキップ if (curTime >= note.stopTime) return "continue"; // (シークバーで途中から再生時)startTimeが過ぎたものは鳴らさない if (cnt == 0 && curTime > note.startTime + baseLatency) return "continue"; // 演奏開始時間 - 先読み時間(ノート予約) になると演奏予約or演奏開始 if (curTime < note.startTime - states.updateBufTime / 1000) return "break"; // PicoAudio音源の再生処理 // if (!settings.isWebMIDI) { // 予約ノート数が急激に増えそうな時、先読み量を小さくしておく // if (states.stopFuncs.length >= 350 && states.updateBufTime < 1000) { states.updateBufTime = 12; states.updateBufMaxTime = states.updateBufTime; } // レトロモード(和音制限モード) // if (settings.maxPoly != -1 || settings.maxPercPoly != -1) { var polyCnt = 0; var percCnt = 0; states.stopFuncs.forEach(function (tar) { if (!tar.note) return; if (tar.note.channel != 9) { if (note.start >= tar.note.start && note.start < tar.note.stop) { polyCnt++; } } else { if (note.start == tar.note.start) { percCnt++; } } }); if (note.channel != 9 && polyCnt >= settings.maxPoly || note.channel == 9 && percCnt >= settings.maxPercPoly) { return "continue"; } } // 1ノート分の再生処理(WebAudioで再生) // var stopFunc = note.channel != 9 ? picoAudio.createNote(note) : picoAudio.createPercussionNote(note); if (!stopFunc) return "continue"; // 無音の場合、処理しない picoAudio.pushFunc({ note: note, stopFunc: stopFunc }); } states.noteOnAry.push(note); }; for (; idx < notes.length; idx++) { var _ret = _loop(); if (_ret === "continue") continue; if (_ret === "break") break; } // notesのどこまで再生したかを記憶して、次回コールバック時にそこから処理を始める states.playIndices[ch] = idx; } // noteOnの時間になったか監視 // this.checkNoteOn(picoAudio); // noteOffの時間になったか監視 // this.checkNoteOff(picoAudio); // WebMIDIの再生処理 // if (settings.isWebMIDI && settings.WebMIDIPortOutput != null) { var messages = picoAudio.playData.messages; var smfData = picoAudio.playData.smfData; var _idx = states.playIndices[16]; // 17chはWebMIDI用 for (; _idx < messages.length; _idx++) { var message = messages[_idx]; var curTime = context.currentTime - states.startTime; // 終わったノートは演奏せずにスキップ if (curTime > message.time + 1) continue; // 演奏開始時間 - 先読み時間(ノート予約) になると演奏予約or演奏開始 if (curTime < message.time - 1) break; // WebMIDIでMIDIメッセージを送信する処理 // var pLen = message.smfPtrLen; var p = message.smfPtr; var time = message.time; var state = smfData[p]; if (state != 0xff) { try { if (state == 0xF0 || state == 0xF7) { // sysExのMIDIメッセージ if (settings.WebMIDIPortSysEx) { // 長さ情報を取り除いて純粋なSysExメッセージにする var lengthAry = ParseUtil.variableLengthToInt(smfData, p + 1, p + 1 + 4); var sysExStartP = p + 1 + lengthAry[1]; var sysExEndP = sysExStartP + lengthAry[0]; var webMIDIMes = new Uint8Array(1 + lengthAry[0]); webMIDIMes[0] = state; var size = sysExEndP - sysExStartP; for (var i = 0; i < size; i++) { webMIDIMes[i + 1] = smfData[sysExStartP + i]; } settings.WebMIDIPortOutput.send(webMIDIMes, (time - context.currentTime + Performance.now() / 1000 + states.startTime) * 1000); } } else { // sysEx以外のMIDIメッセージ var sendMes = []; for (var _i = 0; _i < pLen; _i++) { sendMes.push(smfData[p + _i]); } settings.WebMIDIPortOutput.send(sendMes, (time - context.currentTime + Performance.now() / 1000 + states.startTime) * 1000); } } catch (e) { console.log(e, p, pLen, time, state); } } } // messagesのどこまで送信したかを記憶して、次回コールバック時にそこから処理を始める states.playIndices[16] = _idx; } // 1msコールバックが呼ばれた回数をカウント cnt++; // 変数を反映 // this.updatePreTime = updateNowTime; this.pPreTime = pPreTime; this.cPreTime = cPreTime; this.pTimeSum = pTimeSum; this.cTimeSum = cTimeSum; this.cnt = cnt; } /** * noteOnの時間になったか監視 * @param {PicoAudio} picoAudio PicoAudioインスタンス */ }, { key: "checkNoteOn", value: function checkNoteOn(picoAudio) { var context = picoAudio.context; var trigger = picoAudio.trigger; var states = picoAudio.states; var noteOnAry = picoAudio.states.noteOnAry; var noteOffAry = picoAudio.states.noteOffAry; for (var i = 0; i < noteOnAry.length; i++) { var tempNote = noteOnAry[i]; var nowTime = context.currentTime - states.startTime; if (tempNote.startTime - nowTime <= 0) { ArrayUtil["delete"](noteOnAry, i); // noteOnAry.splice(i, 1); の高速化 noteOffAry.push(tempNote); // イベント発火 if (trigger.isNoteTrigger) trigger.noteOn(tempNote); picoAudio.fireEvent('noteOn', tempNote); i--; } } } /** * noteOffの時間になったか監視 * @param {PicoAudio} picoAudio PicoAudioインスタンス */ }, { key: "checkNoteOff", value: function checkNoteOff(picoAudio) { var context = picoAudio.context; var trigger = picoAudio.trigger; var states = picoAudio.states; var noteOffAry = picoAudio.states.noteOffAry; for (var i = 0; i < noteOffAry.length; i++) { var tempNote = noteOffAry[i]; var nowTime = context.currentTime - states.startTime; if (tempNote.channel != 9 && tempNote.stopTime - nowTime <= 0 || tempNote.channel == 9 && tempNote.drumStopTime - nowTime <= 0) { ArrayUtil["delete"](noteOffAry, i); // noteOffAry.splice(i, 1); の高速化 picoAudio.clearFunc("note", tempNote); // イベント発火 if (trigger.isNoteTrigger) trigger.noteOff(tempNote); picoAudio.fireEvent('noteOff', tempNote); i--; } } } }]); return UpdateNote; }(); function play(isSongLooping) { var _this = this; var context = this.context; var settings = this.settings; var trigger = this.trigger; var states = this.states; // Chrome Audio Policy 対策 // if (context.resume) context.resume(); // 再生中の場合、処理しない // if (states.isPlaying) return; // WebMIDIの場合、少し待ってから再生する // if (settings.isWebMIDI && !isSongLooping) { // Web MIDI API使用時はstop()から800ms程待機すると音がバグりにくい if (states.webMIDIWaitState != "completed") { if (states.webMIDIWaitState != "waiting") { // play()連打の対策 // stop()から1000ms後にplay()を実行 states.webMIDIWaitState = "waiting"; var waitTime = 1000 - (context.currentTime - states.webMIDIStopTime) * 1000; if (states.webMIDIStopTime == 0) waitTime = 1000; // MIDI Portをopenして最初に呼び出すときも少し待つ setTimeout(function () { states.webMIDIWaitState = "completed"; states.isPlaying = false; _this.play(); }, waitTime); } return; } else { states.webMIDIWaitState = null; } } // 変数を用意 // var currentTime = context.currentTime; this.isPlayed = true; states.isPlaying = true; states.startTime = !states.startTime && !states.stopTime ? currentTime : states.startTime + currentTime - states.stopTime; states.stopFuncs = []; // 冒頭の余白をスキップ // if (settings.isSkipBeginning) { var firstNoteOnTime = this.firstNoteOnTime; if (-states.startTime + currentTime < firstNoteOnTime) { this.setStartTime(firstNoteOnTime + states.startTime - currentTime); } } // 曲終了コールバックを予約 // var reserveSongEnd; var reserveSongEndFunc = function reserveSongEndFunc() { _this.clearFunc("rootTimeout", reserveSongEnd); var finishTime = _this.getTime(Number_MAX_SAFE_INTEGER); if (finishTime - context.currentTime + states.startTime <= 0) { // 予定の時間以降に曲終了 trigger.songEnd(); _this.onSongEnd(); _this.fireEvent('songEnd'); } else { // 処理落ちしたりしてまだ演奏中の場合、1ms後に曲終了コールバックを呼び出すよう予約 reserveSongEnd = setTimeout(reserveSongEndFunc, 1); _this.pushFunc({ rootTimeout: reserveSongEnd, stopFunc: function stopFunc() { clearTimeout(reserveSongEnd); } }); } }; var finishTime = this.getTime(Number_MAX_SAFE_INTEGER); var reserveSongEndTime = (finishTime - context.currentTime + states.startTime) * 1000; reserveSongEnd = setTimeout(reserveSongEndFunc, reserveSongEndTime); this.pushFunc({ rootTimeout: reserveSongEnd, stopFunc: function stopFunc() { clearTimeout(reserveSongEnd); } }); // 再生開始をコールバックに通知 // trigger.play(); this.fireEvent('play'); // 1ms毎コールバックの準備 // UpdateNote.init(this, currentTime); // 1ms毎コールバックを開始 // var reserve = setInterval(function () { UpdateNote.update(_this); }, 1); this.pushFunc({ rootTimeout: reserve, stopFunc: function stopFunc() { clearInterval(reserve); } }); } function stop(isSongLooping) { var _this = this; var states = this.states; // 再生していない場合、何もしない // if (states.isPlaying == false) return; // ステータスを停止状態にする・終了処理を呼ぶ // states.isPlaying = false; states.stopTime = this.context.currentTime; states.stopFuncs.forEach(function (n) { // 再生中の音の停止関数を呼ぶ n.stopFunc(); }); states.stopFuncs = []; states.playIndices.forEach(function (n, i, ary) { ary[i] = 0; }); states.noteOnAry = []; states.noteOffAry = []; // WebMIDIで再生中の場合、停止メッセージを送信 // if (this.settings.isWebMIDI) { if (isSongLooping) return; if (this.settings.WebMIDIPortOutput == null) return; states.webMIDIStopTime = this.context.currentTime; setTimeout(function () { for (var t = 0; t < 16; t++) { _this.settings.WebMIDIPortOutput.send([0xB0 + t, 120, 0]); } }, 1000); } // 停止をコールバックに通知 // this.trigger.stop(); this.fireEvent('pause'); this.fireEvent('stop'); } function createBaseNote(option, isDrum, isExpression, nonChannel, nonStop) { var _this = this; // 最低限の変数を準備(無音の場合は処理終了するため) // var settings = this.settings; var context = this.context; var songStartTime = this.states.startTime; var baseLatency = this.baseLatency; var channel = nonChannel ? 0 : option.channel || 0; var velocity = option.velocity * Number(nonChannel ? 1 : this.channels[channel][2] != null ? this.channels[channel][2] : 1) * settings.generateVolume; var isGainValueZero = true; // 無音の場合は処理終了 // if (velocity <= 0) return { isGainValueZero: true }; // 音量の変化を設定 // var expGainValue = velocity * ((option.expression ? option.expression[0].value : 100) / 127); var expGainNode = context.createGain(); expGainNode.gain.value = expGainValue; if (isExpression) { option.expression ? option.expression.forEach(function (p) { var v = velocity * (p.value / 127); if (v > 0) isGainValueZero = false; var t = Math.max(0, p.time + songStartTime + baseLatency); expGainNode.gain.setValueAtTime(v, t); }) : false; } else { if (expGainValue > 0) { isGainValueZero = false; } } // 無音の場合は処理終了 // if (isGainValueZero) { // 音量が常に0なら音を鳴らさない return { isGainValueZero: true }; } // 全ての変数を準備 // var start = option.startTime + songStartTime + baseLatency; var stop = option.stopTime + songStartTime + baseLatency; var pitch = settings.basePitch * Math.pow(Math.pow(2, 1 / 12), (option.pitch || 69) - 69); var oscillator = !isDrum ? context.createOscillator() : context.createBufferSource(); var panNode = context.createStereoPanner ? context.createStereoPanner() : context.createPanner ? context.createPanner() : { pan: { setValueAtTime: function setValueAtTime() {} } }; var gainNode = context.createGain(); var stopGainNode = context.createGain(); // ドラムはホワイトノイズ、ドラム以外はoscillatorを設定 // // oscillatorはピッチ変動も設定 // if (!isDrum) { oscillator.type = option.type || "sine"; oscillator.detune.value = 0; oscillator.frequency.value = pitch; option.pitchBend ? option.pitchBend.forEach(function (p) { var t = Math.max(0, p.time + songStartTime + baseLatency); oscillator.frequency.setValueAtTime(settings.basePitch * Math.pow(Math.pow(2, 1 / 12), option.pitch - 69 + p.value), t); }) : false; } else { oscillator.loop = true; oscillator.buffer = this.whitenoise; } // パンの初期値を設定 // var panValue = option.pan && option.pan[0].value != 64 ? option.pan[0].value / 127 * 2 - 1 : 0; initPanValue(context, panNode, panValue); // パンの変動を設定 // if (context.createStereoPanner || context.createPanner) { // StereoPannerNode or PannerNode がどちらかでも使える var firstNode = true; if (context.createStereoPanner) { // StereoPannerNode が使える option.pan ? option.pan.forEach(function (p) { if (firstNode) { firstNode = false; return; } var v = Math.min(1.0, p.value == 64 ? 0 : p.value / 127 * 2 - 1); var t = Math.max(0, p.time + songStartTime + baseLatency); panNode.pan.setValueAtTime(v, t); }) : false; } else if (context.createPanner) { // StereoPannerNode が未サポート、PannerNode が使える if (panNode.positionX) { // setValueAtTimeが使える // Old Browser var firstPan = true; option.pan ? option.pan.forEach(function (p) { if (firstPan) { firstPan = false; return; } var v = p.value == 64 ? 0 : p.value / 127 * 2 - 1; var posObj = convPosition(v); var t = Math.max(0, p.time + songStartTime + baseLatency); panNode.positionX.setValueAtTime(posObj.x, t); panNode.positionY.setValueAtTime(posObj.y, t); panNode.positionZ.setValueAtTime(posObj.z, t); }) : false; } else { // iOS // setValueAtTimeが使えないためsetTimeoutでパンの動的変更 option.pan ? option.pan.forEach(function (p) { if (firstNode) { firstNode = false; return; } var reservePan = setTimeout(function () { _this.clearFunc("pan", reservePan); var v = Math.min(1.0, p.value == 64 ? 0 : p.value / 127 * 2 - 1); var posObj = convPosition(v); panNode.setPosition(posObj.x, posObj.y, posObj.z); }, (p.time + songStartTime + baseLatency - context.currentTime) * 1000); _this.pushFunc({ pan: reservePan, stopFunc: function stopFunc() { clearTimeout(reservePan); } }); }) : false; } } oscillator.connect(panNode); panNode.connect(expGainNode); } else { // StereoPannerNode、PannerNode が未サポート oscillator.connect(expGainNode); } // AudioNodeを接続 // expGainNode.connect(gainNode); gainNode.connect(stopGainNode); stopGainNode.connect(this.masterGainNode); this.masterGainNode.connect(context.destination); // モジュレーションの変動を設定 // var modulationOscillator; var modulationGainNode; if (!isDrum && option.modulation && (option.modulation.length >= 2 || option.modulation[0].value > 0)) { modulationOscillator = context.createOscillator(); modulationGainNode = context.createGain(); var _firstNode = true; option.modulation ? option.modulation.forEach(function (p) { if (_firstNode) { _firstNode = false; return; } var m = Math.min(1.0, p.value / 127); var t = Math.max(0, p.time + songStartTime + baseLatency); modulationGainNode.gain.setValueAtTime(pitch * 10 / 440 * m, t); }) : false; var m = Math.min(1.0, option.modulation ? option.modulation[0].value / 127 : 0); modulationGainNode.gain.value = pitch * 10 / 440 * m; modulationOscillator.frequency.value = 6; modulationOscillator.connect(modulationGainNode); modulationGainNode.connect(oscillator.frequency); } // リバーブの変動を設定 // if (this.settings.isReverb && option.reverb && (option.reverb.length >= 2 || option.reverb[0].value > 0)) { var convolver = this.convolver; var convolverGainNode = context.createGain(); var _firstNode2 = true; option.reverb ? option.reverb.forEach(function (p) { if (_firstNode2) { _firstNode2 = false; return; } var r = Math.min(1.0, p.value / 127); var t = Math.max(0, p.time + songStartTime + baseLatency); convolverGainNode.gain.setValueAtTime(r, t); }) : false; var r = Math.min(1.0, option.reverb ? option.reverb[0].value / 127 : 0); convolverGainNode.gain.value = r; gainNode.connect(stopGainNode); stopGainNode.connect(convolverGainNode); convolverGainNode.connect(convolver); } // コーラスの変動を設定 // if (this.settings.isChorus && option.chorus && (option.chorus.length >= 2 || option.chorus[0].value > 0)) { var chorusDelayNode = this.chorusDelayNode; var chorusGainNode = context.createGain(); var _firstNode3 = true; option.chorus ? option.chorus.forEach(function (p) { if (_firstNode3) { _firstNode3 = false; return; } var c = Math.min(1.0, p.value / 127); var t = Math.max(0, p.time + songStartTime + baseLatency); chorusGainNode.gain.setValueAtTime(c, t); }) : false; var c = Math.min(1.0, option.chorus ? option.chorus[0].value / 127 : 0); chorusGainNode.gain.value = c; gainNode.connect(stopGainNode); stopGainNode.connect(chorusGainNode); chorusGainNode.connect(chorusDelayNode); } // モジュレーションをスタート // if (modulationOscillator) { modulationOscillator.start(start); this.stopAudioNode(modulationOscillator, stop, modulationGainNode); } // oscillator又はホワイトノイズをスタート // oscillator.start(start); if (!isDrum && !nonChannel && !nonStop) { this.stopAudioNode(oscillator, stop, stopGainNode); } // AudioNodeやパラメータを返す // return { start: start, stop: stop, pitch: pitch, channel: channel, velocity: velocity, oscillator: oscillator, panNode: panNode, gainNode: gainNode, stopGainNode: stopGainNode, isGainValueZero: false }; } /** * パンの初期値を設定 * @param {PannerNode | StereoPannerNode} panNode * @param {number} panValue */ function initPanValue(context, panNode, panValue) { if (context.createStereoPanner) { if (panValue > 1.0) panValue = 1.0; panNode.pan.value = panValue; } else if (context.createPanner) { // iOS, Old Browser var posObj = convPosition(panValue); panNode.panningModel = "equalpower"; panNode.setPosition(posObj.x, posObj.y, posObj.z); } } /** * pan値を基に、PannerNode用の値を{x, y, z}で返す * @param {number} panValue panの値 * @returns Object{x, y, z} */ function convPosition(panValue) { if (panValue > 1.0) panValue = 1.0; var obj = {}; var panAngle = panValue * 90; obj.x = Math.sin(panAngle * (Math.PI / 180)); obj.y = 0; obj.z = -Math.cos(panAngle * (Math.PI / 180)); return obj; } function createNote(option) { var _this = this; var note = this.createBaseNote(option, false, true, false, true); // oscillatorのstopはこちらで実行するよう指定 if (note.isGainValueZero) return null; var oscillator = note.oscillator; var gainNode = note.gainNode; var stopGainNode = note.stopGainNode; var isPizzicato = false; var isNoiseCut = false; var note2; // 音色の設定 // switch (this.channels[note.channel][0] * 1000 || option.instrument) { // Sine case 1000: case 6: case 15: case 24: case 26: case 46: case 50: case 51: case 52: case 53: case 54: case 82: case 85: case 86: { oscillator.type = "sine"; gainNode.gain.value *= 1.5; break; } // Square case 2000: case 4: case 12: case 13: case 16: case 19: case 20: case 32: case 34: case 45: case 48: case 49: case 55: case 56: case 57: case 61: case 62: case 63: case 71: case 72: case 73: case 74: case 75: case 76: case 77: case 78: case 79: case 80: case 84: { oscillator.type = "square"; gainNode.gain.value *= 0.8; break; } // Sawtooth case 3000: case 0: case 1: case 2: case 3: case 7: case 17: case 18: case 21: case 22: case 23: case 27: case 28: case 29: case 30: case 36: case 37: case 38: case 39: case 40: case 41: case 42: case 43: case 44: case 47: case 59: case 64: case 65: case 66: case 67: case 68: case 69: case 70: case 87: { oscillator.type = "sawtooth"; break; } // Triangle case 4000: case 8: case 9: case 10: case 11: case 14: case 25: case 31: case 33: case 35: case 58: case 60: case 83: case 88: case 89: case 90: case 91: case 92: case 93: case 94: case 95: { oscillator.type = "triangle"; gainNode.gain.value *= 1.5; break; } // Other - Square default: { oscillator.type = "s