├── .babelrc ├── .circleci └── config.yml ├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── build └── ongaq.js ├── karma.conf.js ├── package-lock.json ├── package.json ├── readme.md ├── sample ├── index.html ├── script.example.js ├── script.js └── wall.png ├── src ├── Constants │ ├── DRUM_NOTE.js │ ├── ENDPOINT.js │ ├── ROOT.js │ ├── SCHEME.js │ └── VERSION.js ├── Helper │ ├── Chord.js │ ├── Filter.js │ └── shiftKeys.js ├── Ongaq │ ├── Ongaq.js │ ├── module │ │ ├── AudioCore.js │ │ ├── BufferYard.js │ │ ├── Cacher.js │ │ ├── DictPool.js │ │ ├── Helper.js │ │ ├── Part.js │ │ ├── Pool.js │ │ ├── defaults.js │ │ ├── inspect.js │ │ ├── isActive.js │ │ ├── isDrumNoteName.js │ │ ├── make.js │ │ ├── make │ │ │ ├── makeAudioBuffer.js │ │ │ ├── makeDelay.js │ │ │ └── makePanner.js │ │ ├── pool.delay.js │ │ ├── pool.delayfunction.js │ │ ├── pool.element.js │ │ ├── pool.gain.js │ │ ├── pool.pan.js │ │ ├── pool.panfunction.js │ │ ├── toDrumNoteName.js │ │ └── toPianoNoteName.js │ └── plugin │ │ └── filtermapper │ │ ├── PRIORITY.js │ │ ├── arpeggio.js │ │ ├── empty.js │ │ ├── index.js │ │ ├── note.js │ │ ├── pan.js │ │ └── phrase.js └── api.js ├── test ├── cases.js ├── execute.entry.js └── test.webpack.config.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"] 3 | } 4 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/python:3.6-jessie 6 | working_directory: ~/build 7 | steps: 8 | - checkout 9 | - run: 10 | name: install awscli 11 | command: sudo pip install awscli 12 | - run: 13 | name: deploy to S3 14 | command: aws s3 sync build s3://${ONGAQJS_ASSET__S3}/ --acl public-read --delete 15 | - run: 16 | name: purge cache 17 | command: aws cloudfront create-invalidation --distribution-id ${ONGAQJS_ASSET__CLOUDFRONT} --paths "/*" 18 | workflows: 19 | version: 2 20 | build: 21 | jobs: 22 | - build: 23 | context: settings 24 | filters: 25 | branches: 26 | only: master 27 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | 9 | # Matches multiple files with brace expansion notation 10 | # Set default charset 11 | [*] 12 | charset = utf-8 13 | 14 | # 2 space indentation 15 | [*] 16 | indent_style = space 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "globals": { 8 | "Atomics": "readonly", 9 | "SharedArrayBuffer": "readonly" 10 | }, 11 | "parserOptions": { 12 | "ecmaVersion": 2018, 13 | "sourceType": "module" 14 | }, 15 | "plugins": [], 16 | "rules": { 17 | "indent": [ 18 | "error", 19 | 4 20 | ], 21 | "linebreak-style": [ 22 | "error", 23 | "unix" 24 | ], 25 | "quotes": [ 26 | "error", 27 | "double" 28 | ], 29 | "semi": [ 30 | "error", 31 | "never" 32 | ] 33 | } 34 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .sass-cache 3 | thumbs.db 4 | node_modules 5 | *.log 6 | yarn.lock 7 | test/test.bundle.js -------------------------------------------------------------------------------- /build/ongaq.js: -------------------------------------------------------------------------------- 1 | !function(t){var e={};function r(n){if(e[n])return e[n].exports;var i=e[n]={i:n,l:!1,exports:{}};return t[n].call(i.exports,i,i.exports,r),i.l=!0,i.exports}r.m=t,r.c=e,r.d=function(t,e,n){r.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:n})},r.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},r.t=function(t,e){if(1&e&&(t=r(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var i in t)r.d(n,i,function(e){return t[e]}.bind(null,i));return n},r.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return r.d(e,"a",e),e},r.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},r.p="",r(r.s=9)}([function(t,e,r){var n;"undefined"!=typeof window?n=window:"undefined"!=typeof self?n=self:(console.warn("Using browser-only version of superagent in non-browser environment"),n=this);var i=r(3),s=r(4),o=r(1),a=r(5),u=r(7);function h(){}var c=e=t.exports=function(t,r){return"function"==typeof r?new e.Request("GET",t).end(r):1==arguments.length?new e.Request("GET",t):new e.Request(t,r)};e.Request=_,c.getXHR=function(){if(!(!n.XMLHttpRequest||n.location&&"file:"==n.location.protocol&&n.ActiveXObject))return new XMLHttpRequest;try{return new ActiveXObject("Microsoft.XMLHTTP")}catch(t){}try{return new ActiveXObject("Msxml2.XMLHTTP.6.0")}catch(t){}try{return new ActiveXObject("Msxml2.XMLHTTP.3.0")}catch(t){}try{return new ActiveXObject("Msxml2.XMLHTTP")}catch(t){}throw Error("Browser-only version of superagent could not find XHR")};var l="".trim?function(t){return t.trim()}:function(t){return t.replace(/(^\s*|\s*$)/g,"")};function p(t){if(!o(t))return t;var e=[];for(var r in t)d(e,r,t[r]);return e.join("&")}function d(t,e,r){if(null!=r)if(Array.isArray(r))r.forEach((function(r){d(t,e,r)}));else if(o(r))for(var n in r)d(t,e+"["+n+"]",r[n]);else t.push(encodeURIComponent(e)+"="+encodeURIComponent(r));else null===r&&t.push(encodeURIComponent(e))}function f(t){for(var e,r,n={},i=t.split("&"),s=0,o=i.length;s=2&&t._responseTimeoutTimer&&clearTimeout(t._responseTimeoutTimer),4==r){var n;try{n=e.status}catch(t){n=0}if(!n){if(t.timedout||t._aborted)return;return t.crossDomainError()}t.emit("end")}};var n=function(e,r){r.total>0&&(r.percent=r.loaded/r.total*100),r.direction=e,t.emit("progress",r)};if(this.hasListeners("progress"))try{e.onprogress=n.bind(null,"download"),e.upload&&(e.upload.onprogress=n.bind(null,"upload"))}catch(t){}try{this.username&&this.password?e.open(this.method,this.url,!0,this.username,this.password):e.open(this.method,this.url,!0)}catch(t){return this.callback(t)}if(this._withCredentials&&(e.withCredentials=!0),!this._formData&&"GET"!=this.method&&"HEAD"!=this.method&&"string"!=typeof r&&!this._isHost(r)){var i=this._header["content-type"],s=this._serializer||c.serialize[i?i.split(";")[0]:""];!s&&m(i)&&(s=c.serialize["application/json"]),s&&(r=s(r))}for(var o in this.header)null!=this.header[o]&&this.header.hasOwnProperty(o)&&e.setRequestHeader(o,this.header[o]);return this._responseType&&(e.responseType=this._responseType),this.emit("request",this),e.send(void 0!==r?r:null),this},c.agent=function(){return new u},["GET","POST","OPTIONS","PATCH","PUT","DELETE"].forEach((function(t){u.prototype[t.toLowerCase()]=function(e,r){var n=new c.Request(t,e);return this._setDefaults(n),r&&n.end(r),n}})),u.prototype.del=u.prototype.delete,c.get=function(t,e,r){var n=c("GET",t);return"function"==typeof e&&(r=e,e=null),e&&n.query(e),r&&n.end(r),n},c.head=function(t,e,r){var n=c("HEAD",t);return"function"==typeof e&&(r=e,e=null),e&&n.query(e),r&&n.end(r),n},c.options=function(t,e,r){var n=c("OPTIONS",t);return"function"==typeof e&&(r=e,e=null),e&&n.send(e),r&&n.end(r),n},c.del=g,c.delete=g,c.patch=function(t,e,r){var n=c("PATCH",t);return"function"==typeof e&&(r=e,e=null),e&&n.send(e),r&&n.end(r),n},c.post=function(t,e,r){var n=c("POST",t);return"function"==typeof e&&(r=e,e=null),e&&n.send(e),r&&n.end(r),n},c.put=function(t,e,r){var n=c("PUT",t);return"function"==typeof e&&(r=e,e=null),e&&n.send(e),r&&n.end(r),n}},function(t,e,r){"use strict";t.exports=function(t){return null!==t&&"object"==typeof t}},function(t,e,r){var n=r(8);t.exports=function(t,e){return t.set("X-Requested-With","XMLHttpRequest"),t.set("Expires","-1"),t.set("Cache-Control","no-cache,no-store,must-revalidate,max-age=-1,private"),(n||e)&&function(t){var e=Date.now().toString();void 0!==t._query&&t._query[0]?t._query[0]+="&"+e:t._query=[e]}(t),t}},function(t,e,r){function n(t){if(t)return function(t){for(var e in n.prototype)t[e]=n.prototype[e];return t}(t)}t.exports=n,n.prototype.on=n.prototype.addEventListener=function(t,e){return this._callbacks=this._callbacks||{},(this._callbacks["$"+t]=this._callbacks["$"+t]||[]).push(e),this},n.prototype.once=function(t,e){function r(){this.off(t,r),e.apply(this,arguments)}return r.fn=e,this.on(t,r),this},n.prototype.off=n.prototype.removeListener=n.prototype.removeAllListeners=n.prototype.removeEventListener=function(t,e){if(this._callbacks=this._callbacks||{},0==arguments.length)return this._callbacks={},this;var r,n=this._callbacks["$"+t];if(!n)return this;if(1==arguments.length)return delete this._callbacks["$"+t],this;for(var i=0;i=this._maxRetries)return!1;if(this._retryCallback)try{var r=this._retryCallback(t,e);if(!0===r)return!0;if(!1===r)return!1}catch(t){console.error(t)}if(e&&e.status&&e.status>=500&&501!=e.status)return!0;if(t){if(t.code&&~s.indexOf(t.code))return!0;if(t.timeout&&"ECONNABORTED"==t.code)return!0;if(t.crossDomain)return!0}return!1},i.prototype._retry=function(){return this.clearTimeout(),this.req&&(this.req=null,this.req=this.request()),this._aborted=!1,this.timedout=!1,this._end()},i.prototype.then=function(t,e){if(!this._fullfilledPromise){var r=this;this._endCalled&&console.warn("Warning: superagent request was sent twice, because both .end() and .then() were called. Never call .end() if you use promises"),this._fullfilledPromise=new Promise((function(t,e){r.end((function(r,n){r?e(r):t(n)}))}))}return this._fullfilledPromise.then(t,e)},i.prototype.catch=function(t){return this.then(void 0,t)},i.prototype.use=function(t){return t(this),this},i.prototype.ok=function(t){if("function"!=typeof t)throw Error("Callback required");return this._okCallback=t,this},i.prototype._isResponseOK=function(t){return!!t&&(this._okCallback?this._okCallback(t):t.status>=200&&t.status<300)},i.prototype.get=function(t){return this._header[t.toLowerCase()]},i.prototype.getHeader=i.prototype.get,i.prototype.set=function(t,e){if(n(t)){for(var r in t)this.set(r,t[r]);return this}return this._header[t.toLowerCase()]=e,this.header[t]=e,this},i.prototype.unset=function(t){return delete this._header[t.toLowerCase()],delete this.header[t],this},i.prototype.field=function(t,e){if(null==t)throw new Error(".field(name, val) name can not be empty");if(this._data&&console.error(".field() can't be used if .send() is used. Please use only .send() or only .field() & .attach()"),n(t)){for(var r in t)this.field(r,t[r]);return this}if(Array.isArray(e)){for(var i in e)this.field(t,e[i]);return this}if(null==e)throw new Error(".field(name, val) val can not be empty");return"boolean"==typeof e&&(e=""+e),this._getFormData().append(t,e),this},i.prototype.abort=function(){return this._aborted||(this._aborted=!0,this.xhr&&this.xhr.abort(),this.req&&this.req.abort(),this.clearTimeout(),this.emit("abort")),this},i.prototype._auth=function(t,e,r,n){switch(r.type){case"basic":this.set("Authorization","Basic "+n(t+":"+e));break;case"auto":this.username=t,this.password=e;break;case"bearer":this.set("Authorization","Bearer "+t)}return this},i.prototype.withCredentials=function(t){return null==t&&(t=!0),this._withCredentials=t,this},i.prototype.redirects=function(t){return this._maxRedirects=t,this},i.prototype.maxResponseSize=function(t){if("number"!=typeof t)throw TypeError("Invalid argument");return this._maxResponseSize=t,this},i.prototype.toJSON=function(){return{method:this.method,url:this.url,data:this._data,headers:this._header}},i.prototype.send=function(t){var e=n(t),r=this._header["content-type"];if(this._formData&&console.error(".send() can't be used if .attach() or .field() is used. Please use only .send() or only .field() & .attach()"),e&&!this._data)Array.isArray(t)?this._data=[]:this._isHost(t)||(this._data={});else if(t&&this._data&&this._isHost(this._data))throw Error("Can't merge these send calls");if(e&&n(this._data))for(var i in t)this._data[i]=t[i];else"string"==typeof t?(r||this.type("form"),r=this._header["content-type"],this._data="application/x-www-form-urlencoded"==r?this._data?this._data+"&"+t:t:(this._data||"")+t):this._data=t;return!e||this._isHost(t)||r||this.type("json"),this},i.prototype.sortQuery=function(t){return this._sort=void 0===t||t,this},i.prototype._finalizeQueryString=function(){var t=this._query.join("&");if(t&&(this.url+=(this.url.indexOf("?")>=0?"&":"?")+t),this._query.length=0,this._sort){var e=this.url.indexOf("?");if(e>=0){var r=this.url.substring(e+1).split("&");"function"==typeof this._sort?r.sort(this._sort):r.sort(),this.url=this.url.substring(0,e)+"?"+r.join("&")}}},i.prototype._appendQueryString=function(){console.trace("Unsupported")},i.prototype._timeoutError=function(t,e,r){if(!this._aborted){var n=new Error(t+e+"ms exceeded");n.timeout=e,n.code="ECONNABORTED",n.errno=r,this.timedout=!0,this.abort(),this.callback(n)}},i.prototype._setTimeouts=function(){var t=this;this._timeout&&!this._timer&&(this._timer=setTimeout((function(){t._timeoutError("Timeout of ",t._timeout,"ETIME")}),this._timeout)),this._responseTimeout&&!this._responseTimeoutTimer&&(this._responseTimeoutTimer=setTimeout((function(){t._timeoutError("Response timeout of ",t._responseTimeout,"ETIMEDOUT")}),this._responseTimeout))}},function(t,e,r){"use strict";var n=r(6);function i(t){if(t)return function(t){for(var e in i.prototype)t[e]=i.prototype[e];return t}(t)}t.exports=i,i.prototype.get=function(t){return this.header[t.toLowerCase()]},i.prototype._setHeaderProperties=function(t){var e=t["content-type"]||"";this.type=n.type(e);var r=n.params(e);for(var i in r)this[i]=r[i];this.links={};try{t.link&&(this.links=n.parseLinks(t.link))}catch(t){}},i.prototype._setStatusProperties=function(t){var e=t/100|0;this.status=this.statusCode=t,this.statusType=e,this.info=1==e,this.ok=2==e,this.redirect=3==e,this.clientError=4==e,this.serverError=5==e,this.error=(4==e||5==e)&&this.toError(),this.created=201==t,this.accepted=202==t,this.noContent=204==t,this.badRequest=400==t,this.unauthorized=401==t,this.notAcceptable=406==t,this.forbidden=403==t,this.notFound=404==t,this.unprocessableEntity=422==t}},function(t,e,r){"use strict";e.type=function(t){return t.split(/ *; */).shift()},e.params=function(t){return t.split(/ *; */).reduce((function(t,e){var r=e.split(/ *= */),n=r.shift(),i=r.shift();return n&&i&&(t[n]=i),t}),{})},e.parseLinks=function(t){return t.split(/ *, */).reduce((function(t,e){var r=e.split(/ *; */),n=r[0].slice(1,-1);return t[r[1].split(/ *= */)[1].slice(1,-1)]=n,t}),{})},e.cleanHeader=function(t,e){return delete t["content-type"],delete t["content-length"],delete t["transfer-encoding"],delete t.host,e&&(delete t.authorization,delete t.cookie),t}},function(t,e){function r(){this._defaults=[]}["use","on","once","set","query","type","accept","auth","withCredentials","sortQuery","retry","ok","redirects","timeout","buffer","serialize","parse","ca","key","pfx","cert"].forEach((function(t){r.prototype[t]=function(){return this._defaults.push({fn:t,arguments:arguments}),this}})),r.prototype._setDefaults=function(t){this._defaults.forEach((function(e){t[e.fn].apply(t,e.arguments)}))},t.exports=r},function(t,e){t.exports=function(){for(var t=3,e=document.createElement("b"),r=e.all||[];e.innerHTML="\x3c!--[if gt IE "+ ++t+"]>4?t:document.documentMode}()},function(t,e,r){"use strict";r.r(e);var n={};r.r(n),r.d(n,"empty",(function(){return ot})),r.d(n,"note",(function(){return pt})),r.d(n,"arpeggio",(function(){return ft})),r.d(n,"pan",(function(){return _t}));const i=window.innerWidth,s=window.innerHeight,o=t=>{const e=t.listener;return e.forwardX?(e.forwardX.setValueAtTime(0,t.currentTime),e.forwardY.setValueAtTime(0,t.currentTime),e.forwardZ.setValueAtTime(-1,t.currentTime),e.upX.setValueAtTime(0,t.currentTime),e.upY.setValueAtTime(1,t.currentTime),e.upZ.setValueAtTime(0,t.currentTime)):e.setOrientation(0,0,-1,0,1,0),e.positionX?(e.positionX.value=i/2,e.positionY.value=s/2,e.positionZ.value=300):e.setPosition(i/2,s/2,300),t},a=o(new(window.AudioContext||window.webkitAudioContext)),u=(new Date).getTime(),h=(()=>{let t=window.navigator.userAgent;return["iPhone","iPad","iPod","Android"].some(e=>-1!==t.indexOf(e))?"low":"middle"})();var c={context:a,originTime:u,powerMode:h,SUPPRESSION:.5,toAudioBuffer:({src:t,length:e,arrayBuffer:r})=>!(!(r||t&&e)||r&&r instanceof ArrayBuffer==!1)&&new Promise(async(n,i)=>{try{let s;if(r)s=r;else{s=new ArrayBuffer(e);let r=new Uint8Array(s);for(let n=0;nt?n(t):i(),i)}catch(t){i(t)}}),createOfflineContext:({seconds:t})=>o(new OfflineAudioContext(2,44100*t,44100)),spaceWidth:i,spaceHeight:s};var l=new Map([["C",1],["C#",2],["D",3],["Db",2],["D#",4],["E",5],["Eb",4],["F",6],["F#",7],["G",8],["Gb",7],["G#",9],["A",10],["Ab",9],["A#",11],["B",12],["Bb",11]]);const p=/^([A-Z])+([1-4])+(b|#)?$/;var d=(t="")=>{if(!1===p.test(t))return t;{const e=p.exec(t);return e&&l.get(e[1])?`${e[2]}$${l.get(e[1]+(e[3]||""))}`:t}};var f=new Map([["kick","1$1"],["kick2","2$1"],["hihat","1$2"],["hihat2","2$2"],["snare","1$3"],["snare2","2$3"],["tom","1$4"],["tom2","2$4"],["side","1$9"],["side2","2$9"],["cymbal","1$12"],["cymbal2","2$12"]]);const m=window.sessionStorage,y=(()=>{if(!m)return!1;let t=!1;try{m.setItem("_test","1"),t=!0}catch(t){return!1}finally{t&&m.removeItem("_test")}return t})();var _=(t,e)=>!(!y||"string"!=typeof t)&&m.setItem("cache."+t,e),g=t=>!(!y||"string"!=typeof t)&&m.getItem("cache."+t),b=t=>!(!y||"string"!=typeof t)&&m.removeItem("cache."+t),w=r(0),v=r.n(w),T=r(2),x=r.n(T);let E=new Map;const A=t=>{try{return t=(t=t.split("|")).map(t=>{const e=t.split("$");return[e[0],JSON.parse(e[1])]}),new Map(t)}catch(t){return null}};var k=new class{constructor(){this.soundNameMap=new Map}bringIn({sound:t,data:e}){return new Promise((r,n)=>{if("string"!=typeof t||"object"!=typeof e||0===!Object.keys(e).length)return n("invalid options");if((()=>{const e=A(g("soundNameMap"));return e&&e.get(t)})())return n("cannot overwrite official instruments");try{let i=E.get(t)||new Map;Object.keys(e).forEach(async t=>{let r;if(d(t)!==t&&(r=d(t)),!r)return n(`[ ${t} ] is not a valid sound name of original instrument. Use as same notation as for piano like "C1" or "D2#".`);if(e[t]instanceof ArrayBuffer==!1)return n(`value corresponding to [ ${t} ] must be an ArrayBuffer instance`);const s=await c.toAudioBuffer({arrayBuffer:e[t]});i.set(r,s)}),E.set(t,i),r()}catch(t){n("invalid options")}})}getSoundNameMap(){try{const t=A(g("soundNameMap"));return t.forEach(t=>{t.id<2e4?t.type="scalable":t.id<3e4?t.type="percussive":t.type="scalable",delete t.id}),t}catch(t){return null}}set({api_key:t}){this.api_key=t;let e=g("soundNameMap");if(e)try{this.soundNameMap=A(e)}catch(t){throw b("soundNameMap"),new Error("Cannot download instrumental master data.")}else v.a.get("https://api.ongaqjs.com/soundnamemap/").then(t=>{if(!t||200!==t.body.statusCode)throw new Error("Cannot download instrumental master data.");this.soundNameMap=new Map(t.body.data);const e=t.body.data.map(t=>`${t[0]}$${JSON.stringify(t[1])}`).join("|");_("soundNameMap",e)}).catch(()=>{throw new Error("Cannot download instrumental master data.")})}async import({sound:t}){return new Promise((e,r)=>{if(E.get(t))return e();{const e=this.getSoundNameMap();if(e&&!e.get(t))return r(`define instrument [ ${t} ] with Ongaq.bringIn() first`)}E.set(t,[]),v.a.get("https://api.ongaqjs.com/sounds/").query({sound:t,api_key:this.api_key}).set("Content-Type","application/json").use(x.a).then(n=>{let i=n.body.sounds[0];if(!i||"OK"!==i.status)return r();let s="string"==typeof i.data?JSON.parse(i.data):i.data,o=Object.keys(s.note),a=new Map,u=0;o.forEach(async n=>{let i=s.note[n];try{let r=await c.toAudioBuffer({src:i.src,length:i.length});a.set(n,r),++u===o.length&&(o=null,E.set(t,a),e())}catch(e){E.has(t)&&E.delete(t),r()}})}).catch(()=>{E.has(t)&&E.delete(t),r(`Cannot load sound resources. There are 3 possible reasons: 1) [ ${t} ] is invalid as an instrumental name. 2) Your remote IP address or hostname is not registered as an authorized origin at dashboard. 3) [ ${this.api_key} ] is not a valid API key.`)})})}ship({sound:t,key:e}){if(!t||!E.get(t))return!1;const r=this.soundNameMap.get(t)&&this.soundNameMap.get(t).id;return e?(e=r<2e4?d(e):r<3e4?((t="")=>f.get(t)||t)(e):d(e),Array.isArray(e)?e.map(e=>E.get(t).get(e)).filter(t=>t):"string"==typeof e?[E.get(t).get(e)]:[]):E.get(t)}};var M={BPM:120,MIN_BPM:60,MAX_BPM:180,MEASURE:4,VOLUME:.5,NOTE_VOLUME:.5,BEATS_IN_MEASURE:16,PREFETCH_SECOND:"middle"===c.powerMode?.3:2,WAV_MAX_SECONDS:45};let O=new Float32Array(6);var P={toInt:(t,e={})=>{let r="number"==typeof e.max?e.max:Number.POSITIVE_INFINITY,n="number"==typeof e.min?e.min:Number.NEGATIVE_INFINITY,i="number"==typeof e.min?e.base:10,s=parseInt(t,i);return!Number.isNaN(s)&&s<=r&&s>=n&&s},getUUID:t=>{let e,r,n="";for(e=0;e<32;e++)r=16*Math.random()|0,8!=e&&12!=e&&16!=e&&20!=e||(n+="-"),n+=(12==e?4:16==e?3&r|8:r).toString(16);return"number"==typeof t&&(n=n.slice(0,t)),n},getWaveShapeArray:t=>{let e=t&&t>=0&&t<=1?t:M.NOTE_VOLUME;return O[0]=1*e*c.SUPPRESSION,O[1]=.8*e*c.SUPPRESSION,O[2]=.5*e*c.SUPPRESSION,O[3]=.3*e*c.SUPPRESSION,O[4]=.1*e*c.SUPPRESSION,O[5]=0,O},toKeyList:(t,e,r)=>!!t&&(Array.isArray(t)?t:Array.isArray(t.list)?t.list:"function"==typeof t?e>=0&&t(e,r):"string"==typeof t&&[t]),toLength:(t,e,r)=>{switch(typeof t){case"number":return t;case"function":return e>=0&&t(e,r);default:return!1}}};var C=class{constructor(t){this.name=t.name,this.isClass=t.isClass,this.active=!1!==t.active,this.makeMethod=t.makeMethod,this.make=t=>this.isClass?new this.makeMethod(t):this.makeMethod(t),this.pool=[]}allocate(t){let e=void 0;return 0===this.pool.length||!1===this.active?e=this.make(t):(e=this.pool.pop(),e||(e=this.make(t))),e}retrieve(t){this.pool.push(t)}flush(){this.pool=[]}};var S=new C({makeMethod:()=>({}),active:!0,isClass:!1,name:"Element"});var I=new C({makeMethod:t=>t.createGain(),active:!0,isClass:!1,name:"GainNode"});var R=class{constructor(){this.pool=new Map}get(t){return this.pool.get(t)}set(t,e){return this.pool.set(t,e)}flush(){this.pool.forEach(t=>{t.disconnect&&t.disconnect(),t=null}),this.pool=new Map}};var N=new R;let L=[[],[],[],[]],B=[1,2,3,4].map(t=>4*t+c.context.currentTime),$=[];var q={pool:L,periods:B,flush:()=>{L.forEach(t=>{t.forEach((e,r)=>{e&&e.disconnect(),t[r]=null})}),L=[[],[],[],[]],B=[1,2,3,4].map(t=>4*t+c.context.currentTime)},retrieve:t=>{if(t instanceof DelayNode==!1)return!1;t.disconnect(),$.push(t)},allocate:({delayTime:t,end:e},r)=>{let n;if(0===$.length){n=r.createDelay();for(let t=0,r=B.length,i=!1;te&&!i&&(L[t].push(n),i=!0)}else n=$.pop();return n.delayTime.value=t,n}},D=(t="")=>!!f.get(t);const j=new Map,U=new Map;let H=[2,3,4,5].map(t=>4*t+c.context.currentTime);H.forEach(t=>{j.set(t,[]),U.set(t,[])});const X=t=>{const e=t+4;return H=H.slice(1),H.push(e),j.set(e,[]),U.set(e,[]),e};var V=({buffer:t,volume:e},r)=>{r instanceof(window.AudioContext||window.webkitAudioContext)&&(t=>{if(H[0]>t.currentTime)return!1;for(let e=0,r=H.length;et.currentTime||(j.get(H[e])&&j.get(H[e]).forEach(e=>{e.disconnect(),e.context===t&&I.retrieve(e)}),U.get(H[e])&&U.get(H[e]).forEach(t=>{t.disconnect()}),j.delete(H[e]),U.delete(H[e]),X(H[r-1]))})(r);let n=k.ship(t);if(!n)return!1;let i=r.createBufferSource();i.length=t.length,i.buffer=n[0];let s=I.allocate(r);if(s.gain.setValueAtTime(c.SUPPRESSION*("number"==typeof e&&e>=0&&e<1?e:M.NOTE_VOLUME),0),!D(t.key)&&s.gain.setValueCurveAtTime(P.getWaveShapeArray(e),t.startTime+t.length-(.03(z[z.length-1]{q.retrieve(t)}),z=z.slice(1),G=G.slice(1)),q.allocate({delayTime:t,end:e},r));var W=({x:t},e)=>{const r=e.createPanner();r.refDistance=1e3,r.maxDistance=1e4,r.coneOuterGain=1;const n=[1,0,0];r.orientationX?(r.orientationX.setValueAtTime(n[0],e.currentTime),r.orientationY.setValueAtTime(n[1],e.currentTime),r.orientationZ.setValueAtTime(n[2],e.currentTime)):r.setOrientation(...n);const i="number"==typeof(s=t)&&s>=-90&&s<=90?s:0;var s;const o=[c.spaceWidth/2+1e3/c.spaceWidth*c.spaceWidth/90*i/52,c.spaceHeight/2,299];return r.positionX?(r.positionX.setValueAtTime(o[0],e.currentTime),r.positionY.setValueAtTime(o[1],e.currentTime),r.positionZ.setValueAtTime(o[2],e.currentTime)):r.setPosition(...o),r};var K=(t,e,r)=>{switch(t){case"audiobuffer":return V(e,r||c.context);case"delay":return F(e,r||c.context);case"panner":return W(e,r||c.context);default:return null}};var Y=new R;var Z=new R;var J=new Map([["",[4,3]],["M7",[4,3,4]],["7",[4,3,3]],["m",[3,4]],["m7",[3,4,3]],["mM7",[3,4,4]],["6",[4,3,2]],["m6",[3,4,2]],["dim",[3,3]],["aug",[4,4]],["sus4",[5,2]],["7sus4",[5,2,3]],["7-5",[4,2,4]],["m7-5",[3,3,4]],["M7+5",[4,4,3]],["M9",[4,3,4,3]],["m9",[3,4,3,4]],["m11",[3,4,3,7]],["6(9)",[4,3,2,5]],["m6(9)",[3,4,2,5]],["7(b9)",[4,3,3,3]],["9",[4,3,3,4]],["7(9)",[4,3,3,4]],["7(#9)",[4,3,3,5]],["11",[4,3,3,7]],["7(#11)",[4,3,3,8]],["13",[4,3,3,11]],["7(13)",[4,3,3,11]]]);const Q=()=>{I.flush(),S.flush(),N.flush(),Y.flush(),q.flush(),Z.flush()};var tt=class{constructor(t){this._init(t)}add(t){return new Promise(async(e,r)=>{if("function"!=typeof t.loadSound)return r("not a Part object");t.bpm=t.bpm||this.bpm,this.parts.set(t.id,t);try{await t.loadSound();let r=!0;this.parts.forEach(t=>{(t._isLoading||t._loadingFailed)&&(r=!1)}),r&&(this.allPartsLoadedOnce||this.onReady&&this.onReady(),this.allPartsLoadedOnce=!0),e()}catch(t){this.isError||(this.onError&&this.onError(t),this.isError=!0),r(t)}})}bringIn({sound:t,data:e}){return k.bringIn({sound:t,data:e})}prepare({sound:t}){return k.import({sound:t})}start(){return this.isPlaying||0===this.parts.size||(this.isPlaying=!0,this.commonGain||(this.commonGain=this._getCommonGain(c.context)),this.parts.forEach(t=>{t._putTimerRight(c.context.currentTime)}),this._scheduler=window.setInterval(()=>{this._routine(c.context,t=>{this._connect(t)})},"middle"===c.powerMode?50:200)),!1}sound(t={}){if(!t.key||!t.sound)return!1;try{const e=Array.isArray(t.key)?t.key:[t.key],r=t.step>0?t.step:0,n=e.map((e,n)=>K("audiobuffer",{buffer:{sound:t.sound,length:t.second>0?t.second:1.5,key:e,startTime:c.context.currentTime+.1+n*r},volume:1},c.context)).filter(t=>t),i=this._getCommonGain(c.context);n.map(t=>t.connect(i))}catch(t){return!1}}record(t={}){if(this.isPlaying)throw"cannot start recording while playing sounds";if(this.isRecording)throw"cannot start recording while other recording process ongoing";if(!window.OfflineAudioContext)throw"OfflineAudioContext is not supported";return!this.isPlaying&&0!==this.parts.size&&new Promise(async(e,r)=>{try{this.isRecording=!0,Q(),this.parts.forEach(t=>{t._reset(),t._putTimerRight(0)});const r="number"==typeof t.seconds&&t.seconds>=1&&t.seconds<=M.WAV_MAX_SECONDS?t.seconds:"number"==typeof t.seconds&&t.seconds<1?1:M.WAV_MAX_SECONDS,n=c.createOfflineContext({seconds:r}),i=this._getCommonGain(n);this._routine(n,t=>{t.terminal.length>0&&t.terminal[t.terminal.length-1].forEach(t=>{t&&t.connect&&t.connect(i)}),t.initialize(),S.retrieve(t)});const s=await n.startRendering();this.isRecording=!1,e(s)}catch(t){this.isRecording=!1,r(t)}finally{Q()}})}pause(){return!!this.isPlaying&&(this._scheduler&&(window.clearInterval(this._scheduler),this._scheduler=null),this.isPlaying=!1,this._removeCommonGain(),!1)}find(...t){let e=[];return 0===t.length||this.parts.forEach(r=>{t.every(t=>r.tags.includes(t))&&e.push(r)}),e}get params(){let t=!1;return this.parts.forEach(e=>{e._isLoading&&(t=!0)}),{loading:t,isPlaying:this.isPlaying,originTime:c.originTime,currentTime:c.context.currentTime,volume:this.volume}}get context(){return c.context}get constants(){return{DRUM_NOTE:f,ROOT:l,SCHEME:J}}get soundNameMap(){return k.getSoundNameMap()}get version(){return"1.5.0"}set volume(t){if("number"!=typeof t||t<0||t>100)return!1;this._volume=t/100*c.SUPPRESSION,this.commonGain&&this.commonGain.gain.setValueAtTime(this._volume,c.context.currentTime+.01)}get volume(){return 100*this._volume/c.SUPPRESSION}set bpm(t){let e=P.toInt(t,{max:M.MAX_BPM,min:M.MIN_BPM});if(!e)return!1;this._bpm=e,this.parts.forEach(t=>{t.bpm=e})}get bpm(){return this._bpm}_init({api_key:t,volume:e,bpm:r,onReady:n,onError:i}){this.parts=new Map,this.isPlaying=!1,this.isRecording=!1,this.allPartsLoadedOnce=!1,this.volume=e||M.VOLUME,this._nextZeroTime=0,this.bpm=r||M.BPM,"low"===c.powerMode&&window.addEventListener("blur",()=>{this.pause()}),this.onReady="function"==typeof n&&n,this.onError="function"==typeof i&&i,this.isError=!1,this._routine=this._routine.bind(this),k.set({api_key:t})}_getCommonGain(t){const e=t.createDynamicsCompressor();e.connect(t.destination);const r=t.createGain();return r.connect(e),r.gain.setValueAtTime(this._volume,0),r}_removeCommonGain(){return!!this.commonGain&&(this.commonGain.gain.setValueAtTime(0,0),this.commonGain=null,!1)}_connect(t){return!(!t||!this.isPlaying)&&(t.terminal.length>0&&t.terminal[t.terminal.length-1].forEach(t=>{t.connect(this.commonGain)}),t.initialize(),S.retrieve(t),!1)}_routine(t,e){let r,n;return this.parts.forEach(e=>{n=e.collect(t),n&&n.length>0&&(r=r||[],r=r.concat(n))}),!(!r||0===r.length)&&(r.forEach(e),!1)}};var et=(t,e)=>{if(0===t||t<=-13||t>=13)return e;if(!Array.isArray(e))return[];let r=e.map(t=>t.split("$").map(t=>+t));return r=r.map(e=>e[1]+t<=12&&e[1]+t>0?`${e[0]}$${e[1]+t}`:t<0&&e[1]+t<=0?e[0]>1&&`${e[0]-1}$${12+e[1]+t}`:t>0&&e[1]+t>12&&(e[0]<4?`${e[0]+1}$${-12+e[1]+t}`:void 0)).filter(t=>t),r};class rt{constructor(t,e={}){this._init(t,e)}shift(t){return new rt(this.name,{octave:this.defaultOctave,key:et(t,this.originalKey)})}octave(t){if(0===t||"number"!=typeof t||Number.isNaN(t))return this;let e=this.originalKey.map(t=>t.split("$").map(t=>+t));return e=e.map(e=>{if(e[0]+t<=4&&e[0]+t>0)return`${e[0]+t}$${e[1]}`}).filter(t=>t),new rt(this.name,{octave:this.defaultOctave,key:e})}reverse(){let t=this.originalKey.reverse();return new rt(this.name,{octave:this.defaultOctave,key:t})}slice(t,e){if(Number.isNaN(t))return this;let r=this.originalKey.slice(t,e);return new rt(this.name,{octave:this.defaultOctave,key:r})}rotate(){if(!this.key)return this;let t=this.originalKey.map(t=>t),e=t.splice(-1,1)[0],r=t.splice(0,1)[0],n=e.split("$").map(t=>+t),i=r.split("$").map(t=>+t),s=i[1],o=i[1]>n[1]?n[0]:i[1]+n[1]>12?n[0]+1:n[0];if(o>4)return this;let a=this.key.map(t=>t).splice(1);return a.push(`${o}$${s}`),new rt(this.name,{octave:this.defaultOctave,key:a})}get route(){return Array.isArray(this.key)&&this.key[0]}get name(){return this.rootLabel+this.schemeLabel}_init(t,e){if(this.active=!0,this.defaultShift=e.defaultShift||0,this.defaultOctave=e.octave>0&&e.octave<=4?e.octave:2,"string"!=typeof t)return this.active=!1,!1;let r=(()=>{let e,r,n=[];return l.forEach((i,s)=>{n=t.match(new RegExp("^"+s)),n&&n[0]===s&&(e=i,r=s)}),{root:e,rootLabel:r}})();if(!r.root)return this.active=!1,!1;let n=(t=>{let e,r;return J.forEach((n,i)=>{i===t&&(e=n,r=i)}),{scheme:e,schemeLabel:r}})(t.replace(r.rootLabel,""));if(!n.scheme)return this.active=!1,!1;let i=(()=>{if(e.key)return e.key;let t=[],i=r.root,s=this.defaultOctave;return t.push(`${s}$${i}`),n.scheme.forEach(e=>{let r=i+e>12;s=r?s+1:s,i=r?i+e-12:i+e,s<=4&&t.push(`${s}$${i}`)}),t})();this.rootLabel=r.rootLabel,this.defaultOctave=e.octave,this.scheme=n.scheme,this.schemeLabel=n.schemeLabel,this.originalKey=et(this.defaultShift,i.map(t=>t)),this.key=et(this.defaultShift,i)}}var nt=rt,it={empty:10,note:140,notelist:145,arpeggio:240,pan:340};const st=it.empty;var ot=()=>()=>{const t=S.allocate();return t.priority=st,t.terminal=[],t._inits=[],t.initialize=()=>{t._inits.forEach(t=>t())},t};const at=(t,e={},r=!0)=>{let n;switch(typeof t){case"string":return"function"==typeof e.string&&e.string(t);case"object":return Array.isArray(t)&&"function"==typeof e.array?e.array(t):"function"==typeof e.object&&e.object(t);case"number":return"function"==typeof e.number?e.number(t):t;case"boolean":return t;case"function":return n=t(...e._arguments),"function"==typeof e._next&&(n=e._next(n)),r?at(n,e,!1):n;default:return e.default?"function"==typeof e.default?e.default(e._arguments):e.default:t}};var ut=at;var ht=(t,e)=>ut(t,{_arguments:[e.beatIndex,e.measure,e.attachment],object:t=>Array.isArray(t)&&t.includes(e.beatIndex),number:t=>t===e.beatIndex,default:!0});const ct=it.note,lt=[4,32,64];var pt=(t={},e={},r)=>{if(!ht(t.active,e))return!1;const n=ut(t.key,{_arguments:[e.beatIndex,e.measure,e.attachment],string:t=>[t],object:t=>t.key,array:t=>t});if(!n||0===n.length)return!1;const i=ut(t.length,{_arguments:[e.beatIndex,e.measure,e.attachment],number:t=>t,array:t=>t,default:(()=>{const t=k.getSoundNameMap().get(e.sound);return t?t.tag.includes("riff")?lt[2]:"percussive"===t.type?lt[1]:lt[0]:0})()});if(!i)return!1;let s=ut(t.volume,{_arguments:[e.beatIndex,e.measure,e.attachment],number:t=>t>0&&t<100?t/100:0===t?-1:100===t?.999:null,string:()=>!1,object:()=>!1,array:()=>!1});return-1!==s&&(t=>{const o=n.map((t,n)=>K("audiobuffer",{buffer:{sound:e.sound,length:(Array.isArray(i)?"number"==typeof i[n]?i[n]:D(t)?lt[1]:lt[0]:i)*e.secondsPerBeat,key:t,startTime:e.beatTime},volume:s},r));return t.terminal[0]=t.terminal[0]||[],t.terminal[0].push(...o),t.priority=ct,t.footprints=t.footprints||{},t.footprints._noteLength=(Array.isArray(i)?"number"==typeof i[0]?i[0]:lt:i)*e.secondsPerBeat,t.footprints._beatTime=e.beatTime,t})};const dt=it.arpeggio;var ft=(t={},e={},r)=>{if(!ht(t.active,e))return!1;const n=ut(t.step,{number:t=>t<16?t:1,_arguments:[e.beatIndex,e.measure,e.attachment],default:0});if(!n)return!1;const i=ut(t.range,{number:t=>t>0&&t<9?t:3,_arguments:[e.beatIndex,e.measure,e.attachment],default:3}),s=r instanceof(window.AudioContext||window.webkitAudioContext)?`${n}_${i}_${e.secondsPerBeat}`:`offline_${n}_${i}_${e.secondsPerBeat}`;return Z.get(s)||Z.set(s,((t,e,r,n)=>i=>{if(0===i.terminal.length||0===i.terminal[i.terminal.length-1].length)return i;let s=[];for(let o,a=0,u=i.terminal[i.terminal.length-1].length,h=i.footprints._beatTime+i.footprints._noteLength;a{t.connect(o)}),i.terminal.push([o]),i.terminal[i.terminal.length-2].forEach((t,e)=>{t.connect(s[e<=s.length-1?e:s.length-1])}),s=s.slice(0,i.terminal[i.terminal.length-2].length),i.priority=dt,i})(n,i,e.secondsPerBeat,r)),Z.get(s)};const mt=it.pan,yt=(t,e)=>r=>{if(0===r.terminal.length)return r;N.get(t)||N.set(t,K("panner",{x:t},e));const n=N.get(t);return r.terminal.push([n]),r.terminal[r.terminal.length-2].forEach(t=>{t.connect(n)}),r.priority=mt,r};var _t=(t={},e={},r)=>{if(!ht(t.active,e))return!1;const n=ut(t.x,{string:t=>P.toInt(t,{max:90,min:-90}),number:t=>P.toInt(t,{max:90,min:-90}),_arguments:[e.beatIndex,e.measure,e.attachment],_next:t=>P.toInt(t,{max:90,min:-90}),default:0});return!!n&&(r instanceof(window.AudioContext||window.webkitAudioContext)?(Y.get(n)||Y.set(n,yt(n,r)),Y.get(n)):(Y.get("offline_"+n)||Y.set("offline_"+n,yt(n,r)),Y.get("offline_"+n)))};class gt{constructor(t={}){this.sound=t.sound,this.id=t.id||P.getUUID(),this.tags=Array.isArray(t.tags)?t.tags:[],this.bpm=t.bpm,this.measure="number"==typeof t.measure&&t.measure>=0?t.measure:M.MEASURE,this.onLoaded=t&&"function"==typeof t.onLoaded&&t.onLoaded,this.willMakeLap=t&&"function"==typeof t.willMakeLap&&t.willMakeLap,this.maxLap="number"==typeof t.maxLap&&t.maxLap>=0?t.maxLap:1/0,this.repeat=!1!==t.repeat,this._isLoading=!1,this._beatsInMeasure="number"==typeof t.beatsInMeasure&&t.beatsInMeasure>=0?t.beatsInMeasure:M.BEATS_IN_MEASURE,this._currentBeatIndex=0,this._nextBeatTime=0,this._lap=0,this._attachment={},this.default={},this.default.active=!1!==t.active,this.active=!1,this.mute=!!t.mute,this._putTimerRight(c.context.currentTime),this.collect=this.collect.bind(this)}add(t){return!(!t||!t.priority||-1===t.priority)&&(this.filters=this.filters||[],this.filters.push(t),this.filters.sort((t,e)=>t.priority>e.priority?1:t.priority{this._targetBeat=this._targetBeat||{},this._targetBeat.sound=this.sound,this._targetBeat.measure=Math.floor(this._currentBeatIndex/this._beatsInMeasure),this._targetBeat.beatIndex=this._currentBeatIndex%this._beatsInMeasure,this._targetBeat.beatTime=this._nextBeatTime,this._targetBeat.secondsPerBeat=this._secondsPerBeat,this._targetBeat.lap=this._lap,this._targetBeat.attachment=this._attachment;let e=!1,r=[];return this.filters.forEach(({type:i,params:s})=>{if(!Object.hasOwnProperty.call(n,i)||"note"!==i&&"notelist"!==i&&!e)return!1;const o=n[i](s,this._targetBeat,t);o&&("note"!==i&&"notelist"!==i||(e=!0),r.push(o))}),r.reduce((t,e)=>e(t),ot()())},this._generator=this._generator.bind(this),!1)}attach(t={}){this._attachment=Object.assign(this._attachment,t)}collect(t){let e,r=t.currentTime+M.PREFETCH_SECOND+(t instanceof(window.AudioContext||window.webkitAudioContext)?0:M.WAV_MAX_SECONDS);for(;this._nextBeatTime-r>0&&this._nextBeatTime-r=this.measure*this._beatsInMeasure?(this._currentBeatIndex=0,this._lap++,"function"==typeof this.willMakeLap&&this.willMakeLap({nextLap:this._lap,meanTime:this._nextBeatTime}),this._lap>this.maxLap&&(this.repeat?this.resetLap():this.out())):this._currentBeatIndex++}return"function"==typeof this._syncRequest&&(this._syncRequest(),this._syncRequest=null),e}syncTo(t){if(t instanceof gt==!1)return!1;t._syncRequest=()=>{this._currentBeatIndex=(()=>{const e=parseInt(t._currentBeatIndex/(t.measure*t._beatsInMeasure),10);return t._currentBeatIndex-e*(t.measure*t._beatsInMeasure)})(),this._nextBeatTime=t._nextBeatTime}}changeSound({sound:t}){return new Promise(async(e,r)=>{try{await k.import({sound:t}),this.sound=t,e()}catch(t){r(t)}})}detach(t){"string"==typeof t?delete this._attachment[t]:this._attachment={}}in(t){if("number"!=typeof t)throw new Error("assign a number for the first argument for Part.in( )");return this.active||(this._meanTime=t,this._nextBeatTime=t,this.default.active=!0,this.active=!0),!1}async loadSound(){return this._isLoading=!0,new Promise(async(t,e)=>{try{await k.import({sound:this.sound}),this._isLoading=!1,this.active=this.default.active,this.onLoaded&&this.onLoaded(),t()}catch(t){this._isLoading=!1,this._loadingFailed=!0,e(t)}})}out(t,e=!1){return!!this.active&&(this._endTime?e&&t&&(this._endTime=t):t?this._endTime=t:this._shutdown(),!1)}tag(...t){this.tags=Array.isArray(this.tags)?this.tags:[],t.forEach(t=>{this.tags.includes(t)||this.tags.push(t)})}removeTag(...t){this.tags=Array.isArray(this.tags)?this.tags:[],this.tags=this.tags.filter(e=>!t.includes(e))}resetLap(){this._lap=0}set bpm(t){let e=P.toInt(t,{max:M.MAX_BPM,min:M.MIN_BPM});e&&(this._bpm=e)}get bpm(){return this._bpm}set mute(t){"boolean"==typeof t&&(this._mute=t)}get mute(){return this._mute}_shutdown(){if(!this.active)return!1;this._meanTime=0,this._endTime=0,this._nextBeatTime=0,this.active=!1}_putTimerRight(t){if(!this.active||"number"!=typeof t||t<0)return!1;this._nextBeatTime=t}_reset(){this._lap=0,this._currentBeatIndex=0}get _secondsPerBeat(){return 60/this._bpm/8}}var bt=gt;var wt=class{constructor(t){this.init(t)}init(t={type:null}){this.type="string"==typeof t.type?t.type:"note",this.params=t,this.priority=it[this.type]||-1,delete t.type}};window.Ongaq=window.Ongaq||tt,window.Chord=window.Chord||nt,window.Part=window.Part||bt,window.Filter=window.Filter||wt;e.default={Ongaq:tt,Chord:nt,Part:bt,Filter:wt}}]); -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | /* 2 | Karma - Configuration 3 | http://karma-runner.github.io/3.0/config/configuration-file.html 4 | 5 | */ 6 | module.exports = config => { 7 | config.set({ 8 | frameworks: ['jasmine'], 9 | port: 9876, 10 | files: [ 11 | 'test/test.bundle.js' 12 | ] 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ongaqjs", 3 | "version": "1.5.0", 4 | "description": "Ongaq JS portal site: https://www.ongaqjs.com/", 5 | "main": "gulpfile.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "webpack", 9 | "lint": "eslint ./src", 10 | "lint_fix": "eslint ./src --fix", 11 | "karma": "webpack -p --config test/test.webpack.config.js && ./node_modules/karma/bin/karma start" 12 | }, 13 | "keywords": [], 14 | "author": "info@codeninth.com", 15 | "license": "GNU General Public License v2", 16 | "devDependencies": { 17 | "babel-core": "^6.26.3", 18 | "babel-loader": "^7.1.4", 19 | "babel-preset-env": "^1.7.0", 20 | "babel-preset-react": "^6.24.1", 21 | "eslint": "^5.16.0", 22 | "hard-source-webpack-plugin": "^0.13.1", 23 | "karma": "^4.4.1", 24 | "karma-jasmine": "^2.0.1", 25 | "superagent": "^3.8.2", 26 | "superagent-no-cache": "^0.1.1", 27 | "terser": "^4.8.0", 28 | "webpack": "^4.44.2", 29 | "webpack-cli": "^3.3.12" 30 | }, 31 | "dependencies": { 32 | "serialize-javascript": "^3.1.0", 33 | "set-value": "^4.0.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Ongaq JS Docs 2 | 3 | ## Website 4 | https://www.ongaqjs.com/ 5 | 6 | ## Important Points of Sound Resources 7 | Ongaq JS will use sound resources hosted on api.ongaqjs.com by CodeNinth Ltd. 8 | Some of them will be available only with paid license. 9 | ( Details will be added here ) 10 | Please mind NGs about the sound resources which require paid license. 11 | 12 | ### NGs 13 | - Don't let those resources accessible on internet using your domains. 14 | 15 | If you suspect whether certain cases are OK or not, please required to CodeNinth Ltd. 16 | 17 | ## License 18 | GNU General Public License v2 19 | 20 | © 2019 CodeNinth, Ltd. 21 | -------------------------------------------------------------------------------- /sample/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Ongaq JS Sample 8 | 98 | 99 | 100 |
101 |

Ongaq JS Sample

102 |
    103 |
  • 104 |
105 |
106 | 110 | 111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /sample/script.example.js: -------------------------------------------------------------------------------- 1 | /* 2 | replace "YOUR_API_KEY" below with yours 3 | ================ 4 | */ 5 | 6 | const my_drums = new Part({ 7 | sound: "small_cube_drums" 8 | }) 9 | my_drums.add( 10 | new Filter({ 11 | key: ["kick"], 12 | active: n => n % 8 === 0 13 | }) 14 | ) 15 | my_drums.add( 16 | new Filter({ 17 | key: ["hihat"], 18 | active: n => n % 8 === 4 19 | }) 20 | ) 21 | 22 | const my_guitar = new Part({ 23 | sound: "nylon_guitar", 24 | measure: 4 25 | }) 26 | 27 | my_guitar.add(new Filter({ 28 | key: new Chord("CM9"), 29 | length: 16, 30 | active: (n, m) => n === 0 && m === 1 31 | })) 32 | 33 | const ongaq = new Ongaq({ 34 | api_key: "YOUR_API_KEY", 35 | bpm: 130, 36 | volume: 40, 37 | onReady: () => { 38 | const button = document.getElementById("button") 39 | button.className = "button start" 40 | button.onclick = () => { 41 | if (ongaq.params.isPlaying) { 42 | ongaq.pause() 43 | button.className = "button start" 44 | } else { 45 | ongaq.start() 46 | button.className = "button pause" 47 | } 48 | } 49 | } 50 | }) 51 | ongaq.add(my_drums) 52 | ongaq.add(my_guitar) 53 | -------------------------------------------------------------------------------- /sample/script.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeninth/ongaq-js/881bdccf4a651cc92d7c08af3773841f2d884a9c/sample/script.js -------------------------------------------------------------------------------- /sample/wall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeninth/ongaq-js/881bdccf4a651cc92d7c08af3773841f2d884a9c/sample/wall.png -------------------------------------------------------------------------------- /src/Constants/DRUM_NOTE.js: -------------------------------------------------------------------------------- 1 | const DRUM_NOTE = new Map([ 2 | ["kick","1$1"], 3 | ["kick2","2$1"], 4 | ["hihat","1$2"], 5 | ["hihat2","2$2"], 6 | ["snare","1$3"], 7 | ["snare2","2$3"], 8 | ["tom","1$4"], 9 | ["tom2","2$4"], 10 | ["side","1$9"], 11 | ["side2","2$9"], 12 | ["cymbal","1$12"], 13 | ["cymbal2","2$12"] 14 | ]) 15 | 16 | export default DRUM_NOTE 17 | -------------------------------------------------------------------------------- /src/Constants/ENDPOINT.js: -------------------------------------------------------------------------------- 1 | const ENDPOINT = "https://api.ongaqjs.com" 2 | export default ENDPOINT 3 | -------------------------------------------------------------------------------- /src/Constants/ROOT.js: -------------------------------------------------------------------------------- 1 | const ROOT = new Map([ 2 | ["C",1], 3 | ["C#",2], 4 | ["D",3], 5 | ["Db",2], 6 | ["D#",4], 7 | ["E",5], 8 | ["Eb",4], 9 | ["F",6], 10 | ["F#",7], 11 | ["G",8], 12 | ["Gb",7], 13 | ["G#",9], 14 | ["A",10], 15 | ["Ab",9], 16 | ["A#",11], 17 | ["B",12], 18 | ["Bb",11] 19 | ]) 20 | 21 | export default ROOT 22 | -------------------------------------------------------------------------------- /src/Constants/SCHEME.js: -------------------------------------------------------------------------------- 1 | const SCHEME = new Map([ 2 | ["",[4,3]], 3 | ["M7",[4,3,4]], 4 | ["7",[4,3,3]], 5 | ["m",[3,4]], 6 | ["m7",[3,4,3]], 7 | ["mM7",[3,4,4]], 8 | ["6",[4,3,2]], 9 | ["m6",[3,4,2]], 10 | ["dim",[3,3]], 11 | ["aug",[4,4]], 12 | ["sus4",[5,2]], 13 | ["7sus4",[5,2,3]], 14 | ["7-5",[4,2,4]], 15 | ["m7-5",[3,3,4]], 16 | ["M7+5",[4,4,3]], 17 | ["M9",[4,3,4,3]], 18 | ["m9",[3,4,3,4]], 19 | ["m11",[3,4,3,7]], 20 | ["6(9)",[4,3,2,5]], 21 | ["m6(9)",[3,4,2,5]], 22 | ["7(b9)",[4,3,3,3]], 23 | ["9",[4,3,3,4]], 24 | ["7(9)",[4,3,3,4]], 25 | ["7(#9)",[4,3,3,5]], 26 | ["11",[4,3,3,7]], 27 | ["7(#11)",[4,3,3,8]], 28 | ["13",[4,3,3,11]], 29 | ["7(13)",[4,3,3,11]] 30 | ]) 31 | 32 | export default SCHEME 33 | -------------------------------------------------------------------------------- /src/Constants/VERSION.js: -------------------------------------------------------------------------------- 1 | export default "1.5.0" 2 | -------------------------------------------------------------------------------- /src/Helper/Chord.js: -------------------------------------------------------------------------------- 1 | import ROOT from "../Constants/ROOT" 2 | import SCHEME from "../Constants/SCHEME" 3 | import shiftKeys from "./shiftKeys" 4 | 5 | class Chord { 6 | 7 | /* 8 | raw: string like "C#M7" 9 | o: { 10 | octave: 2, 11 | defaultShift: 0, 12 | key: ["1$5","1$11",...] // use when create new Chord object by Chord.shift() or such methods 13 | } 14 | 15 | { 16 | root: index of root, // 1 17 | rootLabel: readable root: // C 18 | scheme: distances between each neighbor // [3,4,3] 19 | schemeLabel: general label // #M7 20 | key: note names corresponding to sound JSON // ["1$1","1$4"] 21 | } 22 | */ 23 | constructor(raw, o = {}){ 24 | this._init(raw,o) 25 | } 26 | 27 | /* 28 | @shift 29 | shift original chord 30 | */ 31 | shift(v){ 32 | return new Chord(this.name, { 33 | octave: this.defaultOctave, 34 | key: shiftKeys(v, this.originalKey) 35 | }) 36 | } 37 | 38 | /* 39 | @octave 40 | change octave of original chord 41 | */ 42 | octave(v){ 43 | if (v === 0 || typeof v !== "number" || Number.isNaN(v)) return this 44 | 45 | let newList = this.originalKey.map(m => m.split("$").map(n => +n)) 46 | newList = newList.map(pair => { 47 | if (pair[0] + v <= 4 && pair[0] + v > 0) return `${pair[0] + v}$${pair[1]}` 48 | }).filter(pair => pair) 49 | 50 | return new Chord(this.name, { 51 | octave: this.defaultOctave, 52 | key: newList 53 | }) 54 | } 55 | 56 | /* 57 | @reverse 58 | make array of note names upside down 59 | */ 60 | reverse(){ 61 | let newList = this.originalKey.reverse() 62 | return new Chord(this.name, { 63 | octave: this.defaultOctave, 64 | key: newList 65 | }) 66 | } 67 | 68 | /* 69 | @slice 70 | slice array of note names 71 | */ 72 | slice(start,end){ 73 | if (Number.isNaN(start)) return this 74 | let newList = this.originalKey.slice(start, end) 75 | return new Chord(this.name,{ 76 | octave: this.defaultOctave, 77 | key: newList 78 | }) 79 | } 80 | 81 | /* 82 | @rotate 83 | rotate original chord 84 | */ 85 | rotate() { 86 | 87 | if (!this.key) return this 88 | 89 | let duplicated = this.originalKey.map(k => k) 90 | let last = duplicated.splice(-1, 1)[0] 91 | let first = duplicated.splice(0, 1)[0] 92 | let l = last.split("$").map(n => +n) 93 | let f = first.split("$").map(n => +n) 94 | 95 | let rolledKey = f[1] 96 | let rolledOctave = (()=>{ 97 | if(f[1] > l[1]) return l[0] 98 | else if(f[1] + l[1] > 12) return l[0] + 1 99 | else return l[0] 100 | })() 101 | if (rolledOctave > 4) return this 102 | 103 | let newList = this.key.map(k => k).splice(1) 104 | newList.push(`${rolledOctave}$${rolledKey}`) 105 | 106 | return new Chord(this.name, { 107 | octave: this.defaultOctave, 108 | key: newList 109 | }) 110 | 111 | } 112 | 113 | get route(){ return Array.isArray(this.key) && this.key[0] } 114 | 115 | get name(){ return this.rootLabel + this.schemeLabel } 116 | 117 | _init(raw, o) { 118 | 119 | this.active = true 120 | this.defaultShift = o.defaultShift || 0 121 | this.defaultOctave = o.octave > 0 && o.octave <= 4 ? o.octave : 2 122 | 123 | if (typeof raw !== "string") { 124 | this.active = false 125 | return false 126 | } 127 | let rootData = (() => { 128 | let result = [], root, rootLabel 129 | ROOT.forEach((v, k) => { 130 | result = raw.match(new RegExp("^" + k)) 131 | if (result && result[0] === k) { 132 | root = v 133 | rootLabel = k 134 | } 135 | }) 136 | return { root, rootLabel } 137 | })() 138 | if (!rootData.root) { 139 | this.active = false 140 | return false 141 | } 142 | 143 | let chordData = ((chord) => { 144 | let scheme, schemeLabel 145 | SCHEME.forEach((v, k) => { 146 | if (k === chord) { 147 | scheme = v 148 | schemeLabel = k 149 | } 150 | }) 151 | return { scheme, schemeLabel } 152 | })(raw.replace(rootData.rootLabel, "")) 153 | 154 | if (!chordData.scheme) { 155 | this.active = false 156 | return false 157 | } 158 | 159 | let key = (() => { 160 | 161 | if (o.key) return o.key 162 | 163 | let key = [] 164 | let currentKey = rootData.root 165 | let currentOctave = this.defaultOctave 166 | 167 | key.push(`${currentOctave}$${currentKey}`) 168 | 169 | chordData.scheme.forEach(s => { 170 | let doOctaveUp = currentKey + s > 12 171 | currentOctave = doOctaveUp ? currentOctave + 1 : currentOctave 172 | currentKey = doOctaveUp ? currentKey + s - 12 : currentKey + s 173 | if (currentOctave <= 4) key.push(`${currentOctave}$${currentKey}`) 174 | }) 175 | return key 176 | 177 | })() 178 | 179 | this.rootLabel = rootData.rootLabel 180 | this.defaultOctave = o.octave 181 | this.scheme = chordData.scheme 182 | this.schemeLabel = chordData.schemeLabel 183 | this.originalKey = shiftKeys(this.defaultShift, key.map(k => k)) 184 | this.key = shiftKeys(this.defaultShift, key) 185 | 186 | } 187 | 188 | } 189 | 190 | export default Chord 191 | -------------------------------------------------------------------------------- /src/Helper/Filter.js: -------------------------------------------------------------------------------- 1 | import PRIORITY from "../Ongaq/plugin/filtermapper/PRIORITY" 2 | 3 | class Filter { 4 | 5 | constructor(params){ 6 | this.init(params) 7 | } 8 | 9 | init(params = { type: null }){ 10 | this.type = typeof params.type === "string" ? params.type : "note" 11 | this.params = params 12 | this.priority = PRIORITY[ this.type ] || -1 13 | delete params.type 14 | } 15 | 16 | } 17 | 18 | export default Filter 19 | -------------------------------------------------------------------------------- /src/Helper/shiftKeys.js: -------------------------------------------------------------------------------- 1 | const shiftKeys = (v,key)=>{ 2 | 3 | if (v === 0 || v <= -13 || v >= 13) return key 4 | else if (!Array.isArray(key) ) return [] 5 | 6 | let shifted = key.map(m => m.split("$").map(n => +n)) 7 | 8 | shifted = shifted.map(pair => { 9 | if (pair[1] + v <= 12 && pair[1] + v > 0) return `${pair[0]}$${pair[1] + v}` 10 | else if (v < 0 && pair[1] + v <= 0) { 11 | /* 12 | - This note goes down to lower octave 13 | - If octave 1, no more getting down -> skipped 14 | */ 15 | if (pair[0] > 1) return `${ pair[0] - 1 }$${ 12 + pair[1] + v }` 16 | else return false 17 | } 18 | else if (v > 0 && pair[1] + v > 12) { 19 | /* 20 | - This note goes up to higher octave 21 | - If octave 4, no more getting up -> skipped 22 | */ 23 | if (pair[0] < 4) return `${ pair[0] + 1 }$${ -12 + pair[1] + v }` 24 | } else { 25 | return false 26 | } 27 | 28 | }).filter(pair => pair) 29 | 30 | return shifted 31 | } 32 | 33 | export default shiftKeys -------------------------------------------------------------------------------- /src/Ongaq/Ongaq.js: -------------------------------------------------------------------------------- 1 | import AudioCore from "./module/AudioCore" 2 | import BufferYard from "./module/BufferYard" 3 | import Helper from "./module/Helper" 4 | import DEFAULTS from "./module/defaults" 5 | import ElementPool from "./module/pool.element" 6 | import GainPool from "./module/pool.gain" 7 | import PanPool from "./module/pool.pan" 8 | import DelayPool from "./module/pool.delay" 9 | import make from "./module/make" 10 | import PanFunctionPool from "./module/pool.panfunction" 11 | import DelayFunctionPool from "./module/pool.delayfunction" 12 | import DRUM_NOTE from "../Constants/DRUM_NOTE" 13 | import ROOT from "../Constants/ROOT" 14 | import SCHEME from "../Constants/SCHEME" 15 | import VERSION from "../Constants/VERSION" 16 | 17 | const flushAll = ()=>{ 18 | GainPool.flush() 19 | ElementPool.flush() 20 | PanPool.flush() 21 | PanFunctionPool.flush() 22 | DelayPool.flush() 23 | DelayFunctionPool.flush() 24 | } 25 | class Ongaq { 26 | 27 | constructor(o) { 28 | this._init(o) 29 | } 30 | 31 | /* 32 | @add 33 | */ 34 | add(part) { 35 | 36 | return new Promise(async(resolve, reject) => { 37 | 38 | if (typeof part.loadSound !== "function") return reject("not a Part object") 39 | 40 | part.bpm = part.bpm || this.bpm 41 | this.parts.set(part.id, part) 42 | 43 | try { 44 | await part.loadSound() 45 | let isAllPartsLoaded = true 46 | /* 47 | when all parts got loaded own sound, 48 | fire this.onReady 49 | */ 50 | this.parts.forEach(p => { 51 | if (p._isLoading || p._loadingFailed) isAllPartsLoaded = false 52 | }) 53 | if (isAllPartsLoaded) { 54 | if (!this.allPartsLoadedOnce) this.onReady && this.onReady() 55 | this.allPartsLoadedOnce = true 56 | } 57 | resolve() 58 | } catch (e) { 59 | if (!this.isError) { 60 | this.onError && this.onError(e) 61 | this.isError = true 62 | } 63 | reject(e) 64 | } 65 | 66 | }) 67 | } 68 | 69 | /* 70 | @bringIn 71 | - bring in original sounds 72 | */ 73 | bringIn({ sound, data }){ 74 | return BufferYard.bringIn({ sound, data }) 75 | } 76 | 77 | /* 78 | @prepare 79 | */ 80 | prepare({ sound }) { 81 | return BufferYard.import({ sound }) 82 | } 83 | 84 | /* 85 | @start 86 | - start executing .collect() at regular interval 87 | */ 88 | start() { 89 | if (this.isPlaying || this.parts.size === 0) return false 90 | this.isPlaying = true 91 | 92 | if (!this.commonGain) this.commonGain = this._getCommonGain(AudioCore.context) 93 | 94 | this.parts.forEach(p => { p._putTimerRight(AudioCore.context.currentTime) }) 95 | 96 | this._scheduler = window.setInterval(() => { 97 | this._routine( 98 | AudioCore.context, 99 | elem => { this._connect(elem) } 100 | ) 101 | }, AudioCore.powerMode === "middle" ? 50 : 200) 102 | return false 103 | } 104 | 105 | sound(o = {}){ 106 | if (!o.key || !o.sound) return false 107 | try { 108 | const keys = Array.isArray(o.key) ? o.key : [o.key] 109 | const step = o.step > 0 ? o.step : 0 110 | const ab = keys.map((key,i)=>{ 111 | return make("audiobuffer", { 112 | buffer: { 113 | sound: o.sound, 114 | length: o.second > 0 ? o.second : 1.5, 115 | key, 116 | startTime: (AudioCore.context.currentTime + 0.1) + i * step 117 | }, 118 | volume: 1 119 | }, AudioCore.context) 120 | }).filter(_=>_) 121 | const g = this._getCommonGain(AudioCore.context) 122 | ab.map(_=>_.connect(g)) 123 | } catch (e) { 124 | return false 125 | } 126 | } 127 | 128 | record(o = {}) { 129 | if (this.isPlaying) throw "cannot start recording while playing sounds" 130 | else if (this.isRecording) throw "cannot start recording while other recording process ongoing" 131 | else if (!window.OfflineAudioContext) throw "OfflineAudioContext is not supported" 132 | if (this.isPlaying || this.parts.size === 0) return false 133 | 134 | return new Promise(async(resolve, reject) => { 135 | try { 136 | 137 | this.isRecording = true 138 | flushAll() 139 | // ====== calculate the seconds of beats beforehand 140 | this.parts.forEach(p => { 141 | p._reset() 142 | p._putTimerRight(0) 143 | }) 144 | const seconds = (() => { 145 | if (typeof o.seconds === "number" && o.seconds >= 1 && o.seconds <= DEFAULTS.WAV_MAX_SECONDS) return o.seconds 146 | else if (typeof o.seconds === "number" && o.seconds < 1) return 1 147 | else return DEFAULTS.WAV_MAX_SECONDS 148 | })() 149 | const offlineContext = AudioCore.createOfflineContext({ seconds: seconds }) 150 | // ======= 151 | const commonGain = this._getCommonGain(offlineContext) 152 | 153 | this._routine( 154 | offlineContext, 155 | elem => { 156 | if (elem.terminal.length > 0) { 157 | elem.terminal[elem.terminal.length - 1].forEach(t => { 158 | t && t.connect && t.connect(commonGain) 159 | }) 160 | } 161 | elem.initialize() 162 | ElementPool.retrieve(elem) 163 | } 164 | ) 165 | 166 | const buffer = await offlineContext.startRendering() 167 | this.isRecording = false 168 | resolve(buffer) 169 | } catch (e) { 170 | this.isRecording = false 171 | reject(e) 172 | } finally { 173 | flushAll() 174 | } 175 | }) 176 | } 177 | 178 | /* 179 | @pause 180 | */ 181 | pause() { 182 | if (!this.isPlaying) return false 183 | if (this._scheduler) { 184 | window.clearInterval(this._scheduler) 185 | this._scheduler = null 186 | } 187 | this.isPlaying = false 188 | this._removeCommonGain() 189 | return false 190 | } 191 | 192 | /* 193 | @find 194 | collect part by tags 195 | */ 196 | find(...tags) { 197 | 198 | let result = [] 199 | if (tags.length === 0) return result 200 | this.parts.forEach(p => { 201 | if (tags.every(tag => p.tags.includes(tag))) result.push(p) 202 | }) 203 | return result 204 | 205 | } 206 | 207 | /* 208 | @get params 209 | */ 210 | get params() { 211 | let loading = false 212 | this.parts.forEach(p => { if (p._isLoading) loading = true }) 213 | return { 214 | loading: loading, 215 | isPlaying: this.isPlaying, 216 | originTime: AudioCore.originTime, 217 | currentTime: AudioCore.context.currentTime, 218 | volume: this.volume 219 | } 220 | } 221 | 222 | get context() { 223 | return AudioCore.context 224 | } 225 | 226 | get constants() { 227 | return { 228 | DRUM_NOTE, 229 | ROOT, 230 | SCHEME 231 | } 232 | } 233 | 234 | get soundNameMap() { 235 | return BufferYard.getSoundNameMap() 236 | } 237 | 238 | get version() { return VERSION } 239 | 240 | set volume(v) { 241 | if (typeof v !== "number" || v < 0 || v > 100) return false 242 | this._volume = v / 100 * AudioCore.SUPPRESSION 243 | this.commonGain && this.commonGain.gain.setValueAtTime( 244 | this._volume, 245 | AudioCore.context.currentTime + 0.01 246 | ) 247 | } 248 | 249 | get volume() { 250 | return this._volume * 100 / AudioCore.SUPPRESSION 251 | } 252 | 253 | set bpm(v) { 254 | let bpm = Helper.toInt(v, { max: DEFAULTS.MAX_BPM, min: DEFAULTS.MIN_BPM }) 255 | if (!bpm) return false 256 | this._bpm = bpm 257 | this.parts.forEach(p => { p.bpm = bpm }) 258 | } 259 | 260 | get bpm() { 261 | return this._bpm 262 | } 263 | 264 | /* 265 | @_init 266 | */ 267 | 268 | _init({ api_key, volume, bpm, onReady, onError }) { 269 | this.parts = new Map() 270 | this.isPlaying = false 271 | this.isRecording = false 272 | this.allPartsLoadedOnce = false 273 | this.volume = volume || DEFAULTS.VOLUME 274 | 275 | this._nextZeroTime = 0 276 | this.bpm = bpm || DEFAULTS.BPM 277 | if (AudioCore.powerMode === "low") { 278 | window.addEventListener("blur", () => { this.pause() }) 279 | } 280 | this.onReady = typeof onReady === "function" && onReady 281 | this.onError = typeof onError === "function" && onError 282 | this.isError = false 283 | this._routine = this._routine.bind(this) 284 | BufferYard.set({ api_key }) 285 | } 286 | 287 | /* 288 | @_getCommonGain 289 | - すべてのaudioNodeが経由するGainNodeを作成 290 | - playのタイミングで毎回作り直す 291 | */ 292 | _getCommonGain(ctx) { 293 | const comp = ctx.createDynamicsCompressor() 294 | comp.connect(ctx.destination) 295 | const g = ctx.createGain() 296 | g.connect(comp) 297 | g.gain.setValueAtTime(this._volume, 0) 298 | return g 299 | } 300 | 301 | /* 302 | @_removeCommonGain 303 | */ 304 | _removeCommonGain() { 305 | if (!this.commonGain) return false 306 | this.commonGain.gain.setValueAtTime(0, 0) 307 | this.commonGain = null 308 | return false 309 | } 310 | 311 | /* 312 | @_connect 313 | */ 314 | _connect(elem) { 315 | if (!elem || !this.isPlaying) return false 316 | if (elem.terminal.length > 0) { 317 | elem.terminal[elem.terminal.length - 1].forEach(t => { 318 | t.connect(this.commonGain) 319 | }) 320 | } 321 | elem.initialize() 322 | ElementPool.retrieve(elem) 323 | return false 324 | } 325 | 326 | /* 327 | @_routine 328 | - 各partに対してobserveを行う 329 | */ 330 | _routine(ctx, connect) { 331 | let collected, elements 332 | this.parts.forEach(p => { 333 | elements = p.collect(ctx) 334 | if (elements && elements.length > 0) { 335 | collected = collected || [] 336 | collected = collected.concat(elements) 337 | } 338 | }) 339 | if (!collected || collected.length === 0) return false 340 | collected.forEach(connect) 341 | return false 342 | } 343 | 344 | } 345 | 346 | export default Ongaq -------------------------------------------------------------------------------- /src/Ongaq/module/AudioCore.js: -------------------------------------------------------------------------------- 1 | const spaceWidth = window.innerWidth 2 | const spaceHeight = window.innerHeight 3 | 4 | const _setListener = ctx => { 5 | const l = ctx.listener // 625.5, 325, 300 6 | if (l.forwardX) { 7 | l.forwardX.setValueAtTime(0, ctx.currentTime) 8 | l.forwardY.setValueAtTime(0, ctx.currentTime) 9 | l.forwardZ.setValueAtTime(-1, ctx.currentTime) 10 | l.upX.setValueAtTime(0, ctx.currentTime) 11 | l.upY.setValueAtTime(1, ctx.currentTime) 12 | l.upZ.setValueAtTime(0, ctx.currentTime) 13 | } else { 14 | l.setOrientation(0, 0, -1, 0, 1, 0) 15 | } 16 | 17 | if (l.positionX) { 18 | l.positionX.value = spaceWidth / 2 19 | l.positionY.value = spaceHeight / 2 20 | l.positionZ.value = 300 21 | } else { 22 | l.setPosition(spaceWidth / 2, spaceHeight / 2, 300) 23 | } 24 | return ctx 25 | } 26 | 27 | const context = _setListener( 28 | new(window.AudioContext || window.webkitAudioContext)() 29 | ) 30 | const originTime = new Date().getTime() 31 | const powerMode = (() => { 32 | let u = window.navigator.userAgent 33 | if (["iPhone", "iPad", "iPod", "Android"].some(name => u.indexOf(name) !== -1)) return "low" 34 | else return "middle" 35 | })() 36 | 37 | const AudioCore = { 38 | 39 | context, 40 | originTime, 41 | powerMode, 42 | SUPPRESSION: 0.5, // To avoid noise, suppress volume with this value 43 | toAudioBuffer: ({ src, length, arrayBuffer }) => { 44 | if ( 45 | (!arrayBuffer && (!src || !length)) || 46 | arrayBuffer && arrayBuffer instanceof ArrayBuffer === false 47 | ){ 48 | return false 49 | } 50 | 51 | return new Promise( async (resolve, reject) => { 52 | try { 53 | let buffer 54 | if(!arrayBuffer){ 55 | buffer = new ArrayBuffer(length) 56 | let bufView = new Uint8Array(buffer) 57 | for (let i = 0; i < length; i++) bufView[i] = src.charCodeAt(i) 58 | } else { 59 | buffer = arrayBuffer 60 | } 61 | context.decodeAudioData( 62 | buffer, 63 | buffer => buffer ? resolve(buffer) : reject(), 64 | reject 65 | ) 66 | } catch (err) { 67 | reject(err) 68 | } 69 | }) 70 | 71 | }, 72 | 73 | createOfflineContext: ({ seconds }) => { 74 | return _setListener( 75 | new OfflineAudioContext(2, 44100 * seconds, 44100) 76 | ) 77 | }, 78 | spaceWidth, 79 | spaceHeight 80 | } 81 | 82 | 83 | 84 | export default AudioCore -------------------------------------------------------------------------------- /src/Ongaq/module/BufferYard.js: -------------------------------------------------------------------------------- 1 | import AudioCore from "./AudioCore" 2 | import ENDPOINT from "../../Constants/ENDPOINT" 3 | import toPianoNoteName from "./toPianoNoteName" 4 | import toDrumNoteName from "./toDrumNoteName" 5 | import Cacher from "./Cacher" 6 | import request from "superagent" 7 | import nocache from "superagent-no-cache" 8 | let buffers = new Map() 9 | 10 | const cacheToMap = cache => { 11 | try { 12 | cache = cache.split("|") 13 | cache = cache.map(pair => { 14 | const array = pair.split("$") 15 | return [array[0], JSON.parse(array[1])] 16 | }) 17 | return new Map(cache) 18 | } catch (e) { 19 | return null 20 | } 21 | } 22 | 23 | class BufferYard { 24 | 25 | constructor(){ 26 | this.soundNameMap = new Map() 27 | } 28 | 29 | /* 30 | { 31 | sound: 'my-sound-name', 32 | data: { 33 | C1: ArrayBuffer, 34 | D2: ArrayBuffer 35 | } 36 | } 37 | */ 38 | bringIn({ sound, data }){ 39 | 40 | return new Promise((resolve,reject)=>{ 41 | 42 | if( 43 | typeof sound !== "string" || 44 | typeof data !== "object" || 45 | !Object.keys(data).length === 0 46 | ){ 47 | return reject("invalid options") 48 | } else if( 49 | (()=>{ 50 | const map = cacheToMap(Cacher.get("soundNameMap")) 51 | return map && map.get(sound) 52 | })() 53 | ){ 54 | return reject("cannot overwrite official instruments") 55 | } 56 | 57 | try { 58 | let thisSoundBuffers = buffers.get(sound) || new Map() 59 | const keys = Object.keys(data) 60 | keys.forEach( async _key=>{ 61 | // check if _key is valid note name as scalable instrument 62 | let key 63 | if(toPianoNoteName(_key) !== _key) key = toPianoNoteName(_key) 64 | if(!key) return reject(`[ ${_key} ] is not a valid sound name of original instrument. Use as same notation as for piano like "C1" or "D2#".`) 65 | // check if ArrayBuffer is assigned 66 | if( (data[_key] instanceof ArrayBuffer) === false) return reject(`value corresponding to [ ${_key} ] must be an ArrayBuffer instance`) 67 | 68 | const audioBuffer = await AudioCore.toAudioBuffer({ 69 | arrayBuffer: data[_key] 70 | }) 71 | thisSoundBuffers.set(key, audioBuffer) 72 | }) 73 | buffers.set(sound,thisSoundBuffers) 74 | resolve() 75 | } catch(e){ 76 | reject("invalid options") 77 | } 78 | }) 79 | 80 | } 81 | 82 | getSoundNameMap(){ 83 | try { 84 | const map = cacheToMap(Cacher.get("soundNameMap")) 85 | // replace instrument id with its type 86 | map.forEach(dict=>{ 87 | if (dict.id < 20000) dict.type = "scalable" 88 | else if (dict.id < 30000) dict.type = "percussive" 89 | else dict.type = "scalable" 90 | delete dict.id 91 | }) 92 | return map 93 | } catch(e){ 94 | return null 95 | } 96 | } 97 | 98 | set({ api_key }) { 99 | this.api_key = api_key 100 | let cache = Cacher.get("soundNameMap") 101 | if(!cache){ 102 | request 103 | .get(`${ENDPOINT}/soundnamemap/`) 104 | .then(result => { 105 | if (!result || result.body.statusCode !== 200) { 106 | throw new Error("Cannot download instrumental master data.") 107 | } 108 | this.soundNameMap = new Map(result.body.data) 109 | const stringified = result.body.data.map(d => `${d[0]}$${JSON.stringify(d[1])}`).join("|") 110 | Cacher.set("soundNameMap", stringified) 111 | }) 112 | .catch(() => { 113 | throw new Error("Cannot download instrumental master data.") 114 | }) 115 | } else { 116 | /* 117 | use cached string like sound_1,10001|sound_b,10002 118 | */ 119 | try { 120 | this.soundNameMap = cacheToMap(cache) 121 | } catch(e) { 122 | Cacher.purge("soundNameMap") 123 | throw new Error("Cannot download instrumental master data.") 124 | } 125 | 126 | } 127 | 128 | } 129 | 130 | /* 131 | - load soundjsons with SoundFile API 132 | - restore mp3: string -> typedArray -> .mp3 133 | */ 134 | async import({ sound }) { 135 | 136 | return new Promise((resolve, reject) => { 137 | // this sound is already loaded 138 | if (buffers.get(sound)){ 139 | return resolve() 140 | } else { 141 | const map = this.getSoundNameMap() 142 | if(map && !map.get(sound)){ 143 | // sound is brought by user 144 | return reject(`define instrument [ ${sound} ] with Ongaq.bringIn() first`) 145 | } 146 | } 147 | 148 | buffers.set(sound,[]) 149 | request 150 | .get(`${ENDPOINT}/sounds/`) 151 | .query({ 152 | sound: sound, 153 | api_key: this.api_key 154 | }) 155 | .set("Content-Type", "application/json") 156 | .use(nocache) 157 | .then(res => { 158 | 159 | let result = res.body.sounds[0] 160 | if (!result || result.status !== "OK") return reject() 161 | let data = typeof result.data === "string" ? JSON.parse(result.data) : result.data 162 | 163 | let notes = Object.keys(data.note) 164 | let thisSoundBuffers = new Map() 165 | let decodedBufferLength = 0 166 | 167 | notes.forEach(async key => { 168 | 169 | let thisNote = data.note[key] 170 | try { 171 | let audioBuffer = await AudioCore.toAudioBuffer({ 172 | src: thisNote.src, 173 | length: thisNote.length 174 | }) 175 | thisSoundBuffers.set(key, audioBuffer) 176 | if (++decodedBufferLength === notes.length) { 177 | notes = null 178 | buffers.set(sound, thisSoundBuffers) 179 | resolve() 180 | } 181 | } catch(e){ 182 | if (buffers.has(sound)) buffers.delete(sound) 183 | reject() 184 | } 185 | 186 | }) 187 | 188 | 189 | }) 190 | .catch(() => { 191 | if (buffers.has(sound)) buffers.delete(sound) 192 | reject(`Cannot load sound resources. There are 3 possible reasons: 1) [ ${sound} ] is invalid as an instrumental name. 2) Your remote IP address or hostname is not registered as an authorized origin at dashboard. 3) [ ${this.api_key} ] is not a valid API key.`) 193 | }) 194 | 195 | }) 196 | 197 | } 198 | 199 | ship({ sound, key }) { 200 | if (!sound || !buffers.get(sound)) return false 201 | /* 202 | readable note name as "A1","hihat" will be converted here 203 | */ 204 | const soundID = this.soundNameMap.get(sound) && this.soundNameMap.get(sound).id 205 | if(!key) return buffers.get(sound) 206 | 207 | if (soundID < 20000) key = toPianoNoteName(key) 208 | else if (soundID < 30000) key = toDrumNoteName(key) 209 | else if (soundID < 60000) key = toPianoNoteName(key) 210 | else key = toPianoNoteName(key) 211 | 212 | if (Array.isArray(key)) { 213 | return key.map(k => buffers.get(sound).get(k)).filter(b => b) 214 | } else if (typeof key === "string") { 215 | return [buffers.get(sound).get(key)] 216 | } else { 217 | return [] 218 | } 219 | } 220 | 221 | } 222 | 223 | export default new BufferYard() 224 | -------------------------------------------------------------------------------- /src/Ongaq/module/Cacher.js: -------------------------------------------------------------------------------- 1 | const ss = window.sessionStorage 2 | const _isAvailable = (() => { 3 | if (!ss) return false 4 | let _isAvailable = false 5 | try { 6 | ss.setItem("_test", "1") 7 | _isAvailable = true 8 | } catch (e) { 9 | return false 10 | } finally { 11 | if (_isAvailable) ss.removeItem("_test") 12 | } 13 | return _isAvailable 14 | })() 15 | 16 | export default { 17 | set: (key, value)=>{ 18 | if (!_isAvailable || "string" !== typeof key) return false 19 | return ss.setItem(`cache.${key}`, value) 20 | }, 21 | get: (key)=>{ 22 | if (!_isAvailable || "string" !== typeof key) return false 23 | return ss.getItem(`cache.${key}`) 24 | }, 25 | purge: (key)=>{ 26 | if (!_isAvailable || "string" !== typeof key) return false 27 | return ss.removeItem(`cache.${key}`) 28 | } 29 | } -------------------------------------------------------------------------------- /src/Ongaq/module/DictPool.js: -------------------------------------------------------------------------------- 1 | class DictPool { 2 | 3 | constructor() { 4 | this.pool = new Map() 5 | } 6 | 7 | get(key){ 8 | return this.pool.get(key) 9 | } 10 | 11 | set(key,value){ 12 | return this.pool.set(key,value) 13 | } 14 | 15 | flush() { 16 | this.pool.forEach((_) => { 17 | _.disconnect && _.disconnect() 18 | _ = null 19 | }) 20 | this.pool = new Map() 21 | } 22 | 23 | } 24 | 25 | export default DictPool 26 | -------------------------------------------------------------------------------- /src/Ongaq/module/Helper.js: -------------------------------------------------------------------------------- 1 | import AudioCore from "../module/AudioCore" 2 | import defaults from "../module/defaults" 3 | let wave = new Float32Array(6) 4 | 5 | const Helper = { 6 | 7 | /* 8 | @toInt 9 | 指定された範囲の整数かどうかを検証しつつ 10 | 引数を整数に変換する 11 | */ 12 | toInt: (v,o = {})=>{ 13 | let max = typeof o.max === "number" ? o.max : Number.POSITIVE_INFINITY 14 | let min = typeof o.min === "number" ? o.min : Number.NEGATIVE_INFINITY 15 | let base = typeof o.min === "number" ? o.base : 10 16 | let int = parseInt(v,base) 17 | if( 18 | !Number.isNaN(int) && 19 | int <= max && 20 | int >= min 21 | ){ 22 | return int 23 | } else { 24 | return false 25 | } 26 | }, 27 | 28 | /* 29 | @getUUID 30 | uuid文字列を生成する 31 | */ 32 | getUUID: digit=>{ 33 | let uuid = "", i, random 34 | for (i = 0; i < 32; i++) { 35 | random = Math.random() * 16 | 0 36 | if (i == 8 || i == 12 || i == 16 || i == 20) uuid += "-" 37 | uuid += (i == 12 ? 4 : (i == 16 ? (random & 3 | 8) : random)).toString(16) 38 | } 39 | if("number" === typeof digit) uuid = uuid.slice(0,digit) 40 | return uuid 41 | }, 42 | 43 | /* 44 | @getWaveShapeArray 45 | ゆるやかに0に向かうカーブを生成するための配列を返す 46 | */ 47 | getWaveShapeArray: v=>{ 48 | let volume = v && (v >= 0 && v <= 1) ? v : defaults.NOTE_VOLUME 49 | wave[0] = 1 * volume * AudioCore.SUPPRESSION 50 | wave[1] = 0.8 * volume * AudioCore.SUPPRESSION 51 | wave[2] = 0.5 * volume * AudioCore.SUPPRESSION 52 | wave[3] = 0.3 * volume * AudioCore.SUPPRESSION 53 | wave[4] = 0.1 * volume * AudioCore.SUPPRESSION 54 | wave[5] = 0.0 55 | return wave 56 | }, 57 | 58 | /* 59 | @toKeyList 60 | raw: string or array or KeyList or function 61 | 62 | 型がさまざまな可能性のある引数から 63 | ["C2#","A2"] のような配列か false を返す。 64 | */ 65 | toKeyList: (raw,beatIndex,measure)=>{ 66 | if(!raw) return false 67 | else if(Array.isArray(raw)) return raw 68 | else if(Array.isArray(raw.list)) return raw.list 69 | else if (typeof raw === "function") return beatIndex >= 0 && raw(beatIndex, measure) 70 | else if(typeof raw === "string") return [raw] 71 | else return false 72 | }, 73 | 74 | /* 75 | @toLength 76 | raw: string or array or KeyList or function 77 | 78 | 型がさまざまな可能性のある引数から 79 | 何拍分かを表す相対値を整数で返す。 80 | */ 81 | toLength: (raw,beatIndex,measure)=>{ 82 | switch(typeof raw){ 83 | case "number": 84 | return raw 85 | case "function": 86 | return beatIndex >= 0 && raw(beatIndex,measure) 87 | default: 88 | return false 89 | } 90 | } 91 | 92 | } 93 | 94 | export default Helper 95 | -------------------------------------------------------------------------------- /src/Ongaq/module/Part.js: -------------------------------------------------------------------------------- 1 | import AudioCore from "./AudioCore" 2 | import Helper from "./Helper" 3 | import * as filterMapper from "../plugin/filtermapper/index" 4 | import BufferYard from "./BufferYard" 5 | import DEFAULTS from "./defaults" 6 | 7 | class Part { 8 | 9 | constructor(props = {}){ 10 | this.sound = props.sound 11 | this.id = props.id || Helper.getUUID() 12 | this.tags = Array.isArray(props.tags) ? props.tags : [] 13 | this.bpm = props.bpm // bpm is set through ongaq.add 14 | this.measure = (typeof props.measure === "number" && props.measure >= 0) ? props.measure : DEFAULTS.MEASURE 15 | 16 | this.onLoaded = props && typeof props.onLoaded === "function" && props.onLoaded 17 | this.willMakeLap = props && typeof props.willMakeLap === "function" && props.willMakeLap 18 | /* 19 | maxLap: 20 | if the lap would be over maxLap, this Part stops (repeat: false) or its lap returns to 0 (repeat: true) 21 | */ 22 | this.maxLap = (typeof props.maxLap === "number" && props.maxLap >= 0 ) ? props.maxLap : Infinity 23 | this.repeat = props.repeat !== false 24 | 25 | this._isLoading = false 26 | this._beatsInMeasure = (typeof props.beatsInMeasure === "number" && props.beatsInMeasure >= 0) ? props.beatsInMeasure : DEFAULTS.BEATS_IN_MEASURE 27 | this._currentBeatIndex = 0 28 | 29 | /* 30 | @_nextBeatTime 31 | - time for next notepoint 32 | - updated with AudioContext.currentTime 33 | */ 34 | this._nextBeatTime = 0 35 | 36 | /* 37 | @lap 38 | - get added 1 when all beats are observed 39 | */ 40 | this._lap = 0 41 | 42 | /* 43 | @attachment 44 | - conceptual value: user would be able to handle any value to part with this 45 | */ 46 | this._attachment = {} 47 | 48 | this.default = {} 49 | this.default.active = props.active !== false 50 | this.active = false 51 | this.mute = !!props.mute 52 | 53 | this._putTimerRight(AudioCore.context.currentTime) 54 | 55 | this.collect = this.collect.bind(this) 56 | } 57 | 58 | add(newFilter){ 59 | if(!newFilter || !newFilter.priority || newFilter.priority === -1) return false 60 | 61 | this.filters = this.filters || [] 62 | this.filters.push(newFilter) 63 | this.filters.sort((a,b)=>{ 64 | if(a.priority > b.priority) return 1 65 | else if(a.priority < b.priority) return -1 66 | else return 0 67 | }) 68 | 69 | this._generator = ( context ) => { 70 | 71 | this._targetBeat = this._targetBeat || {} 72 | this._targetBeat.sound = this.sound 73 | this._targetBeat.measure = Math.floor(this._currentBeatIndex / this._beatsInMeasure) 74 | this._targetBeat.beatIndex = this._currentBeatIndex % this._beatsInMeasure 75 | this._targetBeat.beatTime = this._nextBeatTime 76 | this._targetBeat.secondsPerBeat = this._secondsPerBeat 77 | this._targetBeat.lap = this._lap 78 | this._targetBeat.attachment = this._attachment 79 | 80 | let hasNote = false 81 | let mapped = [] 82 | this.filters.forEach(({ type, params })=>{ 83 | if( 84 | !Object.hasOwnProperty.call(filterMapper, type) || 85 | ( (type !== "note" && type !== "notelist") && !hasNote ) 86 | ){ return false } 87 | const mappedFunction = filterMapper[type](params, this._targetBeat, context ) 88 | if (mappedFunction){ 89 | if (type === "note" || type === "notelist") hasNote = true 90 | mapped.push( mappedFunction ) 91 | } 92 | }) 93 | return mapped.reduce((accumulatedResult, currentFunction) => { 94 | return currentFunction(accumulatedResult) 95 | }, filterMapper.empty()()) 96 | } 97 | this._generator = this._generator.bind(this) 98 | return false 99 | } 100 | 101 | attach(data = {}) { 102 | this._attachment = Object.assign(this._attachment, data) 103 | } 104 | 105 | collect( ctx ){ 106 | 107 | let collected 108 | 109 | /* 110 | keep _nextBeatTime being always behind secondToPrefetch 111 | */ 112 | let secondToPrefetch = ctx.currentTime + DEFAULTS.PREFETCH_SECOND + (ctx instanceof (window.AudioContext || window.webkitAudioContext) ? 0 : DEFAULTS.WAV_MAX_SECONDS) 113 | while ( 114 | this._nextBeatTime - secondToPrefetch > 0 && 115 | this._nextBeatTime - secondToPrefetch < DEFAULTS.PREFETCH_SECOND 116 | ){ 117 | secondToPrefetch += DEFAULTS.PREFETCH_SECOND 118 | } 119 | /* 120 | if this._endTime is scheduled and secondToPrefetch will be overlap, this Part must stop 121 | */ 122 | if(this._endTime && this._endTime < secondToPrefetch){ 123 | this._shutdown() 124 | } 125 | 126 | /* 127 | collect soundtrees for notepoints which come in certain range 128 | */ 129 | while (this.active && this._nextBeatTime < secondToPrefetch){ 130 | let element = !this.mute && this._generator( ctx ) 131 | if(element){ 132 | collected = collected || [] 133 | collected = collected.concat(element) 134 | } 135 | 136 | this._nextBeatTime += this._secondsPerBeat 137 | 138 | if(this._currentBeatIndex + 1 >= this.measure * this._beatsInMeasure){ 139 | 140 | this._currentBeatIndex = 0 141 | this._lap++ 142 | typeof this.willMakeLap === "function" && this.willMakeLap({ 143 | nextLap: this._lap, 144 | meanTime: this._nextBeatTime 145 | }) 146 | if(this._lap > this.maxLap){ 147 | if (this.repeat) this.resetLap() 148 | else this.out() 149 | } 150 | 151 | } else { 152 | this._currentBeatIndex++ 153 | } 154 | 155 | } 156 | 157 | /* 158 | if there is a request from other part for it to sync to this Part, 159 | execute it here 160 | */ 161 | if(typeof this._syncRequest === "function"){ 162 | this._syncRequest() 163 | this._syncRequest = null 164 | } 165 | 166 | return collected 167 | 168 | } 169 | 170 | syncTo(meanPart){ 171 | if(meanPart instanceof Part === false) return false 172 | meanPart._syncRequest = ()=>{ 173 | this._currentBeatIndex = (()=>{ 174 | const t = parseInt( meanPart._currentBeatIndex / (meanPart.measure * meanPart._beatsInMeasure), 10) 175 | return meanPart._currentBeatIndex - t * (meanPart.measure * meanPart._beatsInMeasure) 176 | })() 177 | this._nextBeatTime = meanPart._nextBeatTime 178 | } 179 | } 180 | 181 | changeSound({ sound }){ 182 | return new Promise( async (resolve,reject)=>{ 183 | try { 184 | await BufferYard.import({ sound }) 185 | this.sound = sound 186 | resolve() 187 | } catch(e){ 188 | reject(e) 189 | } 190 | }) 191 | } 192 | 193 | detach(field) { 194 | if (typeof field === "string") delete this._attachment[field] 195 | else this._attachment = {} 196 | } 197 | 198 | in(meanTime){ 199 | if(typeof meanTime !== "number") throw new Error("assign a number for the first argument for Part.in( )") 200 | if(this.active) return false 201 | this._meanTime = meanTime 202 | this._nextBeatTime = meanTime 203 | this.default.active = true // once in() called, this Part should be paused / restarted as usual 204 | this.active = true 205 | return false 206 | } 207 | 208 | async loadSound(){ 209 | this._isLoading = true 210 | return new Promise( async (resolve,reject)=>{ 211 | try { 212 | await BufferYard.import({ sound: this.sound }) 213 | this._isLoading = false 214 | this.active = this.default.active 215 | this.onLoaded && this.onLoaded() 216 | resolve() 217 | } catch(e) { 218 | this._isLoading = false 219 | this._loadingFailed = true 220 | reject(e) 221 | } 222 | }) 223 | } 224 | 225 | out(endTime,overwrite = false){ 226 | if(!this.active) return false 227 | if(this._endTime){ 228 | // this._endTime is already set. 229 | if(overwrite && endTime) this._endTime = endTime 230 | } else { 231 | // if suitable _endTime is not assigned, shutdown immediately. 232 | if(endTime) this._endTime = endTime 233 | else this._shutdown() 234 | } 235 | return false 236 | } 237 | 238 | /* 239 | @tag 240 | tags: A,B,C... 241 | add tag A, tag B, tag C... 242 | */ 243 | tag(...tags) { 244 | this.tags = Array.isArray(this.tags) ? this.tags : [] 245 | tags.forEach(tag => { 246 | if (!this.tags.includes(tag)) this.tags.push(tag) 247 | }) 248 | } 249 | 250 | removeTag(...tags){ 251 | this.tags = Array.isArray(this.tags) ? this.tags : [] 252 | this.tags = this.tags.filter(tag=>{ 253 | return !tags.includes(tag) 254 | }) 255 | } 256 | 257 | resetLap(){ 258 | this._lap = 0 259 | } 260 | 261 | set bpm(v) { 262 | let bpm = Helper.toInt(v, { max: DEFAULTS.MAX_BPM, min: DEFAULTS.MIN_BPM }) 263 | if (bpm) this._bpm = bpm 264 | } 265 | get bpm() { return this._bpm } 266 | 267 | set mute(v) { 268 | if (typeof v === "boolean") this._mute = v 269 | } 270 | get mute() { return this._mute } 271 | 272 | _shutdown(){ 273 | if(!this.active) return false 274 | this._meanTime = 0 275 | this._endTime = 0 276 | this._nextBeatTime = 0 277 | this.active = false 278 | } 279 | 280 | _putTimerRight(_meanTime){ 281 | if (!this.active || typeof _meanTime !== "number" || _meanTime < 0) return false 282 | this._nextBeatTime = _meanTime 283 | } 284 | 285 | _reset(){ 286 | this._lap = 0 287 | this._currentBeatIndex = 0 288 | } 289 | 290 | get _secondsPerBeat(){ return 60 / this._bpm / 8 } 291 | 292 | } 293 | 294 | export default Part 295 | -------------------------------------------------------------------------------- /src/Ongaq/module/Pool.js: -------------------------------------------------------------------------------- 1 | class Pool { 2 | 3 | constructor(o) { 4 | 5 | this.name = o.name 6 | this.isClass = o.isClass 7 | this.active = o.active !== false 8 | 9 | this.makeMethod = o.makeMethod 10 | this.make = (option) => { 11 | if (this.isClass) return new this.makeMethod(option) 12 | else return this.makeMethod(option) 13 | } 14 | this.pool = [] 15 | 16 | } 17 | 18 | allocate(option) { 19 | 20 | let obj = undefined 21 | if (this.pool.length === 0 || this.active === false) { 22 | obj = this.make(option) 23 | } else { 24 | obj = this.pool.pop() 25 | if (!obj) obj = this.make(option) 26 | } 27 | return obj 28 | } 29 | 30 | retrieve(obj) { 31 | this.pool.push(obj) 32 | } 33 | 34 | flush() { 35 | this.pool = [] 36 | } 37 | 38 | } 39 | 40 | export default Pool 41 | -------------------------------------------------------------------------------- /src/Ongaq/module/defaults.js: -------------------------------------------------------------------------------- 1 | import AudioCore from "./AudioCore" 2 | 3 | const VALUES = { 4 | BPM: 120, 5 | MIN_BPM: 60, 6 | MAX_BPM: 180, 7 | MEASURE: 4, 8 | VOLUME: 0.5, 9 | NOTE_VOLUME: 0.5, 10 | BEATS_IN_MEASURE: 16, 11 | PREFETCH_SECOND: AudioCore.powerMode === "middle" ? 0.3 : 2.0, 12 | WAV_MAX_SECONDS: 45 13 | } 14 | export default VALUES 15 | -------------------------------------------------------------------------------- /src/Ongaq/module/inspect.js: -------------------------------------------------------------------------------- 1 | const inspect = (object, policy = {}, redo = true) => { 2 | let result 3 | switch (typeof object) { 4 | case "string": 5 | return typeof policy.string === "function" && policy.string(object) 6 | case "object": 7 | if (Array.isArray(object) && typeof policy.array === "function") return policy.array(object) 8 | return typeof policy.object === "function" && policy.object(object) 9 | case "number": 10 | return typeof policy.number === "function" ? policy.number(object) : object 11 | case "boolean": 12 | return object 13 | case "function": 14 | result = object(...policy._arguments) 15 | if (typeof policy._next === "function") result = policy._next(result) 16 | return redo ? inspect(result, policy, false) : result 17 | default: 18 | if (policy.default) { 19 | if (typeof policy.default === "function") return policy.default(policy._arguments) 20 | else return policy.default 21 | } else { 22 | return object 23 | } 24 | } 25 | 26 | } 27 | 28 | export default inspect -------------------------------------------------------------------------------- /src/Ongaq/module/isActive.js: -------------------------------------------------------------------------------- 1 | import inspect from "./inspect" 2 | 3 | const isActive = (active,beat) => { 4 | return inspect(active, { 5 | _arguments: [beat.beatIndex, beat.measure, beat.attachment], 6 | object: v => Array.isArray(v) && v.includes(beat.beatIndex), 7 | number: v => v === beat.beatIndex, 8 | default: true 9 | }) 10 | } 11 | 12 | export default isActive 13 | -------------------------------------------------------------------------------- /src/Ongaq/module/isDrumNoteName.js: -------------------------------------------------------------------------------- 1 | import DRUM_NOTE from "../../Constants/DRUM_NOTE" 2 | 3 | export default (raw = "") => !!DRUM_NOTE.get(raw) -------------------------------------------------------------------------------- /src/Ongaq/module/make.js: -------------------------------------------------------------------------------- 1 | import AudioCore from "./AudioCore" 2 | import makeAudioBuffer from "./make/makeAudioBuffer" 3 | import makeDelay from "./make/makeDelay" 4 | import makePanner from "./make/makePanner" 5 | 6 | //============================= 7 | const make = (name, option, context )=>{ 8 | switch(name){ 9 | case "audiobuffer": return makeAudioBuffer(option, context || AudioCore.context ) 10 | case "delay": return makeDelay(option, context || AudioCore.context) 11 | case "panner": return makePanner(option, context || AudioCore.context) 12 | default: return null 13 | } 14 | } 15 | 16 | export default make 17 | -------------------------------------------------------------------------------- /src/Ongaq/module/make/makeAudioBuffer.js: -------------------------------------------------------------------------------- 1 | import Helper from "../Helper" 2 | import BufferYard from "../BufferYard" 3 | import AudioCore from "../AudioCore" 4 | import defaults from "../defaults" 5 | import isDrumNoteName from "../isDrumNoteName" 6 | import gainPool from "../pool.gain" 7 | 8 | const RETRIEVE_INTERVAL = 4 9 | 10 | const gainGarage = new Map() 11 | const bufferSourceGarage = new Map() 12 | let periods = [2, 3, 4, 5].map(n => n * RETRIEVE_INTERVAL + AudioCore.context.currentTime) 13 | periods.forEach(p => { 14 | gainGarage.set(p, []) 15 | bufferSourceGarage.set(p, []) 16 | }) 17 | const addPeriod = minimum => { 18 | const nextPeriod = minimum + RETRIEVE_INTERVAL 19 | periods = periods.slice(1) 20 | periods.push(nextPeriod) 21 | gainGarage.set(nextPeriod, []) 22 | bufferSourceGarage.set(nextPeriod, []) 23 | return nextPeriod 24 | } 25 | 26 | const retrieve = ctx => { 27 | if (periods[0] > ctx.currentTime) return false 28 | for (let i = 0, l = periods.length; i < l; i++) { 29 | if (periods[i] > ctx.currentTime) continue 30 | gainGarage.get(periods[i]) && gainGarage.get(periods[i]).forEach(usedGain => { 31 | usedGain.disconnect() 32 | if (usedGain.context === ctx) { 33 | // when right after context is switched from offline to normal, gainNodes in the garage can not be reused 34 | gainPool.retrieve(usedGain) 35 | } 36 | }) 37 | bufferSourceGarage.get(periods[i]) && bufferSourceGarage.get(periods[i]).forEach(usedSource => { 38 | usedSource.disconnect() 39 | }) 40 | gainGarage.delete(periods[i]) 41 | bufferSourceGarage.delete(periods[i]) 42 | addPeriod(periods[l - 1]) 43 | } 44 | return false 45 | } 46 | 47 | const makeAudioBuffer = ({ buffer, volume }, ctx) => { 48 | 49 | if (ctx instanceof (window.AudioContext || window.webkitAudioContext)) retrieve(ctx) 50 | let audioBuffer = BufferYard.ship(buffer) 51 | if (!audioBuffer) return false 52 | 53 | let s = ctx.createBufferSource() 54 | s.length = buffer.length 55 | s.buffer = audioBuffer[0] 56 | let g = gainPool.allocate(ctx) 57 | g.gain.setValueAtTime(AudioCore.SUPPRESSION * ((typeof volume === "number" && volume >= 0 && volume < 1) ? volume : defaults.NOTE_VOLUME), 0) 58 | // Set end of sound unless the instrument is drums 59 | !isDrumNoteName(buffer.key) && g.gain.setValueCurveAtTime( 60 | Helper.getWaveShapeArray(volume), 61 | buffer.startTime + buffer.length - (0.03 < buffer.length ? 0.03 : buffer.length * 0.6), 62 | 0.03 < buffer.length ? 0.03 : buffer.length * 0.6 63 | ) 64 | s.connect(g) 65 | s.start(buffer.startTime) 66 | 67 | if (!(ctx instanceof (window.AudioContext || window.webkitAudioContext))) return g 68 | 69 | // when normal audioContext, cache node to disconnect after used 70 | for (let i = 0, l = periods.length; i < l; i++) { 71 | if (buffer.startTime + buffer.length + 0.1 < periods[i]) { 72 | gainGarage.get(periods[i]).push(g) 73 | bufferSourceGarage.get(periods[i]).push(s) 74 | break 75 | } 76 | if (i === l - 1) { 77 | const nextPeriod = addPeriod(buffer.startTime + buffer.length + 0.1) 78 | gainGarage.get(nextPeriod).push(g) 79 | bufferSourceGarage.get(nextPeriod).push(s) 80 | } 81 | } 82 | return g 83 | 84 | } 85 | 86 | export default makeAudioBuffer -------------------------------------------------------------------------------- /src/Ongaq/module/make/makeDelay.js: -------------------------------------------------------------------------------- 1 | import DelayPool from "../pool.delay" 2 | let pool = DelayPool.pool 3 | let periods = DelayPool.periods 4 | 5 | const RETRIEVE_INTERVAL = 4 6 | const PADDING = 24 7 | 8 | const makeDelay = ({ delayTime, end }, ctx )=>{ 9 | 10 | if(periods[periods.length-1] < end){ 11 | pool.push([]) 12 | periods.push( end + RETRIEVE_INTERVAL ) 13 | } else if (periods[0] + PADDING < end) { 14 | // to free old delayNodes 15 | pool[0] && pool[0].forEach((usedDelay) => { 16 | DelayPool.retrieve(usedDelay) 17 | }) 18 | periods = periods.slice(1) 19 | pool = pool.slice(1) 20 | } 21 | return DelayPool.allocate({ delayTime, end },ctx) 22 | } 23 | 24 | export default makeDelay 25 | -------------------------------------------------------------------------------- /src/Ongaq/module/make/makePanner.js: -------------------------------------------------------------------------------- 1 | import AudioCore from "../AudioCore" 2 | 3 | const makePanner = ({ x }, ctx) => { 4 | const p = ctx.createPanner() 5 | p.refDistance = 1000 6 | p.maxDistance = 10000 7 | p.coneOuterGain = 1 8 | 9 | const _o = [1, 0, 0] 10 | if (p.orientationX) { 11 | p.orientationX.setValueAtTime(_o[0], ctx.currentTime) 12 | p.orientationY.setValueAtTime(_o[1], ctx.currentTime) 13 | p.orientationZ.setValueAtTime(_o[2], ctx.currentTime) 14 | } else { 15 | p.setOrientation(..._o) 16 | } 17 | 18 | const xValue = ((_x) => (typeof _x === "number" && _x >= -90 && _x <= 90) ? _x : 0)(x) 19 | // mastering in case of width: 1000px -> multiply ratio (1000/AudioCore.spaceWidth) 20 | const _p = [AudioCore.spaceWidth / 2 + (1000 / AudioCore.spaceWidth) * AudioCore.spaceWidth / 90 * xValue / 52, AudioCore.spaceHeight / 2, 299] 21 | 22 | if (p.positionX) { 23 | p.positionX.setValueAtTime(_p[0], ctx.currentTime) 24 | p.positionY.setValueAtTime(_p[1], ctx.currentTime) 25 | p.positionZ.setValueAtTime(_p[2], ctx.currentTime) 26 | } else { 27 | p.setPosition(..._p) 28 | } 29 | 30 | return p 31 | 32 | } 33 | 34 | export default makePanner -------------------------------------------------------------------------------- /src/Ongaq/module/pool.delay.js: -------------------------------------------------------------------------------- 1 | import AudioCore from "./AudioCore" 2 | const RETRIEVE_INTERVAL = 4 3 | 4 | let pool = [ [], [], [], [] ] 5 | let periods = [1, 2, 3, 4].map(n => n * RETRIEVE_INTERVAL + AudioCore.context.currentTime) 6 | let recycleBox = [] 7 | 8 | export default { 9 | pool, 10 | periods, 11 | flush: ()=>{ 12 | pool.forEach(list=>{ 13 | list.forEach((usedDelay,_)=>{ 14 | usedDelay && usedDelay.disconnect() 15 | list[_] = null 16 | }) 17 | }) 18 | pool = [ [], [], [], [] ] 19 | periods = [1, 2, 3, 4].map(n => n * RETRIEVE_INTERVAL + AudioCore.context.currentTime) 20 | }, 21 | retrieve: usedDelay => { 22 | if (usedDelay instanceof DelayNode === false) return false 23 | usedDelay.disconnect() 24 | recycleBox.push(usedDelay) 25 | }, 26 | allocate: ({ delayTime, end },ctx)=>{ 27 | let d 28 | if(recycleBox.length === 0){ 29 | d = ctx.createDelay() 30 | for (let i = 0, l = periods.length, done = false; i < l; i++) { 31 | if (periods[i] > end && !done) { 32 | pool[i].push(d), done = true 33 | } 34 | } 35 | } else { 36 | d = recycleBox.pop() 37 | } 38 | d.delayTime.value = delayTime 39 | return d 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Ongaq/module/pool.delayfunction.js: -------------------------------------------------------------------------------- 1 | import DictPool from "./DictPool" 2 | const pool = new DictPool() 3 | 4 | export default pool -------------------------------------------------------------------------------- /src/Ongaq/module/pool.element.js: -------------------------------------------------------------------------------- 1 | import Pool from "./Pool" 2 | 3 | const pool = new Pool({ 4 | makeMethod: ()=>{ 5 | return {} 6 | }, 7 | active: true, 8 | isClass: false, 9 | name: "Element" 10 | }) 11 | 12 | export default pool 13 | -------------------------------------------------------------------------------- /src/Ongaq/module/pool.gain.js: -------------------------------------------------------------------------------- 1 | import Pool from "./Pool" 2 | 3 | const pool = new Pool({ 4 | makeMethod: context => context.createGain(), 5 | active: true, 6 | isClass: false, 7 | name: "GainNode" 8 | }) 9 | 10 | export default pool 11 | -------------------------------------------------------------------------------- /src/Ongaq/module/pool.pan.js: -------------------------------------------------------------------------------- 1 | import DictPool from "./DictPool" 2 | const pool = new DictPool() 3 | 4 | export default pool -------------------------------------------------------------------------------- /src/Ongaq/module/pool.panfunction.js: -------------------------------------------------------------------------------- 1 | import DictPool from "./DictPool" 2 | const pool = new DictPool() 3 | 4 | export default pool -------------------------------------------------------------------------------- /src/Ongaq/module/toDrumNoteName.js: -------------------------------------------------------------------------------- 1 | import DRUM_NOTE from "../../Constants/DRUM_NOTE" 2 | 3 | /* 4 | convert key name expression 5 | e.g.) "hihat" -> "1$5" 6 | */ 7 | export default (raw = "") => { 8 | return DRUM_NOTE.get(raw) || raw 9 | } -------------------------------------------------------------------------------- /src/Ongaq/module/toPianoNoteName.js: -------------------------------------------------------------------------------- 1 | import ROOT from "../../Constants/ROOT" 2 | 3 | /* 4 | convert key name expression 5 | e.g.) "A1#" -> "1$11" 6 | */ 7 | const r = /^([A-Z])+([1-4])+(b|#)?$/ 8 | 9 | export default (raw = "") => { 10 | if (r.test(raw) === false) { 11 | return raw 12 | } else { 13 | const result = r.exec(raw) 14 | /* 15 | g1: C,D,E...A,B 16 | g2: 1,2,3,4 17 | g3: undefined, b, # 18 | */ 19 | if (!result || !ROOT.get(result[1])) return raw 20 | return `${result[2]}$${ROOT.get(result[1] + (result[3] || ""))}` 21 | } 22 | } -------------------------------------------------------------------------------- /src/Ongaq/plugin/filtermapper/PRIORITY.js: -------------------------------------------------------------------------------- 1 | export default { 2 | empty: 10, 3 | note: 140, 4 | notelist: 145, 5 | arpeggio: 240, 6 | pan: 340 7 | } -------------------------------------------------------------------------------- /src/Ongaq/plugin/filtermapper/arpeggio.js: -------------------------------------------------------------------------------- 1 | import Helper from "../../module/Helper" 2 | import make from "../../module/make" 3 | import inspect from "../../module/inspect" 4 | import isActive from "../../module/isActive" 5 | import DelayFunctionPool from "../../module/pool.delayfunction" 6 | import PRIORITY from "../../plugin/filtermapper/PRIORITY" 7 | const MY_PRIORITY = PRIORITY.arpeggio 8 | 9 | const generate = (step, range, secondsPerBeat, ctx) => { 10 | 11 | return MappedFunction => { 12 | 13 | if ( 14 | MappedFunction.terminal.length === 0 || 15 | MappedFunction.terminal[ MappedFunction.terminal.length - 1 ].length === 0 16 | ) return MappedFunction 17 | 18 | let newNodes = [] 19 | for (let i = 0, max = MappedFunction.terminal[MappedFunction.terminal.length - 1].length, delayTime, end = MappedFunction.footprints._beatTime + MappedFunction.footprints._noteLength; i < max; i++) { 20 | delayTime = secondsPerBeat * (i <= range ? i : range) * step 21 | if (ctx instanceof (window.AudioContext || window.webkitAudioContext)){ 22 | newNodes.push(make("delay", { delayTime, end }, ctx)) 23 | } else { 24 | newNodes.push(make("delay", { delayTime, end }, ctx)) 25 | } 26 | } 27 | 28 | let g = ctx.createGain() 29 | g.gain.setValueAtTime(1, 0) 30 | g.gain.setValueCurveAtTime( 31 | Helper.getWaveShapeArray(0), 32 | MappedFunction.footprints._beatTime + MappedFunction.footprints._noteLength - 0.02, 0.02 33 | ) 34 | newNodes.forEach(n=>{ n.connect(g) }) 35 | 36 | MappedFunction.terminal.push([g]) 37 | MappedFunction.terminal[ MappedFunction.terminal.length - 2 ].forEach((pn, i) => { 38 | pn.connect( newNodes[ i <= newNodes.length - 1 ? i : newNodes.length - 1 ] ) 39 | }) 40 | newNodes = newNodes.slice(0, MappedFunction.terminal[ MappedFunction.terminal.length - 2 ].length) 41 | 42 | MappedFunction.priority = MY_PRIORITY 43 | return MappedFunction 44 | 45 | } 46 | 47 | } 48 | 49 | /* 50 | o: { 51 | step: 0.5 // relative beat length 52 | } 53 | */ 54 | const mapper = (o = {}, _targetBeat = {}, ctx ) => { 55 | 56 | if (!isActive(o.active, _targetBeat)) return false 57 | 58 | const step = inspect(o.step, { 59 | number: v => v < 16 ? v : 1, 60 | _arguments: [_targetBeat.beatIndex, _targetBeat.measure, _targetBeat.attachment], 61 | default: 0 62 | }) 63 | if (!step) return false 64 | const range = inspect(o.range, { 65 | number: v => (v > 0 && v < 9) ? v : 3, 66 | _arguments: [_targetBeat.beatIndex, _targetBeat.measure, _targetBeat.attachment], 67 | default: 3 68 | }) 69 | 70 | const cacheKey = ctx instanceof (window.AudioContext || window.webkitAudioContext) ? `${step}_${range}_${_targetBeat.secondsPerBeat}` : `offline_${step}_${range}_${_targetBeat.secondsPerBeat}` 71 | if (DelayFunctionPool.get(cacheKey)) return DelayFunctionPool.get(cacheKey) 72 | else { 73 | DelayFunctionPool.set(cacheKey, generate(step, range, _targetBeat.secondsPerBeat, ctx)) 74 | return DelayFunctionPool.get(cacheKey) 75 | } 76 | 77 | } 78 | 79 | export default mapper 80 | -------------------------------------------------------------------------------- /src/Ongaq/plugin/filtermapper/empty.js: -------------------------------------------------------------------------------- 1 | import pool from "../../module/pool.element" 2 | import PRIORITY from "../../plugin/filtermapper/PRIORITY" 3 | const MY_PRIORITY = PRIORITY.empty 4 | const Element = ()=>{ 5 | 6 | return ()=>{ 7 | const elem = pool.allocate() 8 | elem.priority = MY_PRIORITY 9 | elem.terminal = [] 10 | elem._inits = [] 11 | elem.initialize = ()=>{ 12 | elem._inits.forEach(i=>i()) 13 | } 14 | return elem 15 | } 16 | 17 | } 18 | 19 | export default Element 20 | -------------------------------------------------------------------------------- /src/Ongaq/plugin/filtermapper/index.js: -------------------------------------------------------------------------------- 1 | // Priority: 10 2 | export { default as empty } 3 | from "./empty" 4 | 5 | // Priority: 140 6 | export { default as note } 7 | from "./note" 8 | 9 | // Priority: 240 10 | export { default as arpeggio } 11 | from "./arpeggio" 12 | 13 | //Priority: 340 14 | export { default as pan } 15 | from "./pan" -------------------------------------------------------------------------------- /src/Ongaq/plugin/filtermapper/note.js: -------------------------------------------------------------------------------- 1 | import make from "../../module/make" 2 | import PRIORITY from "../../plugin/filtermapper/PRIORITY" 3 | import inspect from "../../module/inspect" 4 | import isActive from "../../module/isActive" 5 | import isDrumNoteName from "../../module/isDrumNoteName" 6 | import BufferYard from "../../module/BufferYard" 7 | 8 | const MY_PRIORITY = PRIORITY.note 9 | const DEFAULT_NOTE_LENGTH = [4,32,64] 10 | 11 | /* 12 | o: { 13 | key: ["C1","G1"], 14 | active: n=>n%4 15 | } 16 | */ 17 | const mapper = (o = {}, _targetBeat = {}, context) => { 18 | 19 | if (!isActive(o.active, _targetBeat)) return false 20 | 21 | /* 22 | key should be: 23 | - string like "C1" 24 | - array like ["C1","G1"] 25 | - Chord object 26 | */ 27 | const key = inspect(o.key, { 28 | _arguments: [_targetBeat.beatIndex, _targetBeat.measure, _targetBeat.attachment], 29 | string: v => [v], 30 | object: v => v.key, 31 | array: v => v 32 | }) 33 | if (!key || key.length === 0) return false 34 | 35 | /* 36 | calculate relative length of note 37 | */ 38 | const length = inspect(o.length, { 39 | _arguments: [_targetBeat.beatIndex, _targetBeat.measure, _targetBeat.attachment], 40 | number: v => v, 41 | array: v => v, 42 | default: (()=>{ 43 | const m = BufferYard.getSoundNameMap().get(_targetBeat.sound) 44 | if(!m) return 0 45 | else if (m.tag.includes("riff")) return DEFAULT_NOTE_LENGTH[2] 46 | else if (m.type === "percussive") return DEFAULT_NOTE_LENGTH[1] 47 | else return DEFAULT_NOTE_LENGTH[0] 48 | })() 49 | }) 50 | if (!length) return false 51 | 52 | const _volume_number = v=>{ 53 | if(v > 0 && v < 100) return v / 100 54 | else if(v === 0) return -1 55 | else if(v === 100) return 0.999 56 | else return null 57 | } 58 | let volume = inspect(o.volume, { 59 | _arguments: [_targetBeat.beatIndex, _targetBeat.measure, _targetBeat.attachment], 60 | number: _volume_number, 61 | string: () => false, 62 | object: () => false, 63 | array: () => false 64 | }) 65 | if(volume === -1) return false // to prevent noise when 0 assigned 66 | /* 67 | 必ず自身と同じ構造のオブジェクトを返す関数を返す 68 | ===================================================================== 69 | */ 70 | 71 | return MappedFunction => { 72 | 73 | const newNodes = key.map((k, i) => { 74 | return make("audiobuffer", { 75 | buffer: { 76 | sound: _targetBeat.sound, 77 | length: (!Array.isArray(length) ? 78 | length : 79 | (typeof length[i] === "number" ? 80 | length[i]: (isDrumNoteName(k) ? DEFAULT_NOTE_LENGTH[1] : DEFAULT_NOTE_LENGTH[0])) 81 | ) * _targetBeat.secondsPerBeat, 82 | key: k, 83 | startTime: _targetBeat.beatTime 84 | }, 85 | volume 86 | }, context) 87 | }) 88 | 89 | MappedFunction.terminal[0] = MappedFunction.terminal[0] || [] 90 | MappedFunction.terminal[0].push(...newNodes) 91 | MappedFunction.priority = MY_PRIORITY 92 | MappedFunction.footprints = MappedFunction.footprints || {} 93 | MappedFunction.footprints._noteLength = (!Array.isArray(length) ? length : (typeof length[0] === "number" ? length[0] : DEFAULT_NOTE_LENGTH)) * _targetBeat.secondsPerBeat 94 | MappedFunction.footprints._beatTime = _targetBeat.beatTime 95 | return MappedFunction 96 | 97 | } 98 | 99 | 100 | } 101 | 102 | export default mapper -------------------------------------------------------------------------------- /src/Ongaq/plugin/filtermapper/pan.js: -------------------------------------------------------------------------------- 1 | import Helper from "../../module/Helper" 2 | import make from "../../module/make" 3 | import inspect from "../../module/inspect" 4 | import isActive from "../../module/isActive" 5 | import PanPool from "../../module/pool.pan" 6 | import PanFunctionPool from "../../module/pool.panfunction" 7 | import PRIORITY from "../../plugin/filtermapper/PRIORITY" 8 | const MY_PRIORITY = PRIORITY.pan 9 | 10 | const generate = (x, context) => { 11 | 12 | return MappedFunction => { 13 | if (MappedFunction.terminal.length === 0) return MappedFunction 14 | if (!PanPool.get(x)) PanPool.set(x, make("panner", { x }, context)) 15 | const newNode = PanPool.get(x) 16 | 17 | MappedFunction.terminal.push([newNode]) 18 | 19 | MappedFunction.terminal[MappedFunction.terminal.length - 2].forEach(pn => { 20 | pn.connect(newNode) 21 | }) 22 | MappedFunction.priority = MY_PRIORITY 23 | return MappedFunction 24 | } 25 | 26 | } 27 | 28 | /* 29 | o: { 30 | x: 90 31 | } 32 | */ 33 | const mapper = (o = {}, _targetBeat = {}, context) => { 34 | 35 | if (!isActive(o.active, _targetBeat)) return false 36 | const x = inspect(o.x, { 37 | string: v => Helper.toInt(v, { max: 90, min: -90 }), 38 | number: v => Helper.toInt(v, { max: 90, min: -90 }), 39 | _arguments: [_targetBeat.beatIndex, _targetBeat.measure, _targetBeat.attachment], 40 | _next: v => { 41 | return Helper.toInt(v, { max: 90, min: -90 }) 42 | }, 43 | default: 0 44 | }) 45 | if (!x) return false 46 | 47 | if (!(context instanceof (window.AudioContext || window.webkitAudioContext))) { 48 | if (PanFunctionPool.get(`offline_${x}`)) return PanFunctionPool.get(`offline_${x}`) 49 | else { 50 | PanFunctionPool.set(`offline_${x}`, generate(x, context)) 51 | return PanFunctionPool.get(`offline_${x}`) 52 | } 53 | } else { 54 | if (PanFunctionPool.get(x)) return PanFunctionPool.get(x) 55 | else { 56 | PanFunctionPool.set(x, generate(x, context)) 57 | return PanFunctionPool.get(x) 58 | } 59 | } 60 | 61 | } 62 | 63 | export default mapper -------------------------------------------------------------------------------- /src/Ongaq/plugin/filtermapper/phrase.js: -------------------------------------------------------------------------------- 1 | import Helper from "../../module/Helper" 2 | 3 | //======================================== 4 | /* 5 | o: { 6 | path: [ 7 | [["C2","G2"],8], 8 | [null,4], 9 | [["A2","D2#"],4,0.8] 10 | ], 11 | active: n=>n===0 12 | } 13 | 14 | layer of path: 15 | [ name_of_key, length, volume ] 16 | */ 17 | const mapper = (o = {},beat = {})=>{ 18 | 19 | if(!Array.isArray(o.path) || o.path.length === 0) return false 20 | 21 | let distance = 0 // pathの開始から何拍目に移動したか 22 | let newLayer = [] 23 | let _key, _length // 一時的な値を格納 24 | 25 | o.path.forEach(pair=>{ 26 | 27 | if(pair.length < 2) return false 28 | if(pair[1] > 0) distance += pair[1] 29 | 30 | /* 31 | get key,length as same as "note" mapper 32 | */ 33 | _key = Helper.toKeyList(pair[0], beat.beatIndex, beat.measure ) 34 | _length = Helper.toLength(pair[1], beat.beatIndex, beat.measure ) 35 | if(!_key || !_length) return false 36 | 37 | _key.forEach(k=>{ 38 | newLayer.push({ 39 | invoker: "audioBufferLine", 40 | data: { 41 | buffer: { 42 | sound: beat.sound, 43 | length: pair[1] * beat._secondsPerBeat, 44 | key: k, 45 | startTime: beat.beatTime + distance * beat._secondsPerBeat 46 | }, 47 | volume: pair[2] >= 0 && pair[2] <= 1 ? pair[2] : ( o.volume >= 0 && o.volume <= 100 ? o.volume / 100 : null) 48 | } 49 | }) 50 | }) 51 | 52 | }) 53 | 54 | return newLayer 55 | 56 | } 57 | 58 | export default mapper 59 | -------------------------------------------------------------------------------- /src/api.js: -------------------------------------------------------------------------------- 1 | import Ongaq from "./Ongaq/Ongaq" 2 | import Chord from "./Helper/Chord" 3 | import Part from "./Ongaq/module/Part" 4 | import Filter from "./Helper/Filter" 5 | 6 | window.Ongaq = window.Ongaq || Ongaq 7 | window.Chord = window.Chord || Chord 8 | window.Part = window.Part || Part 9 | window.Filter = window.Filter || Filter 10 | 11 | export default { 12 | Ongaq, 13 | Chord, 14 | Part, // to export to global scope 15 | Filter // to export to global scope 16 | } 17 | -------------------------------------------------------------------------------- /test/cases.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | label: "CM9", 4 | function: ()=>{ 5 | let c = new Chord("CM9",{ octave: 1 }) 6 | return c.key.join(", ") === "1$1, 1$5, 1$8, 1$12, 2$3" 7 | } 8 | }, 9 | { 10 | label: "CM9 shift(1)", 11 | function: () => { 12 | let c = new Chord("CM9", { octave: 1 }) 13 | c = c.shift(1) 14 | return c.key.join(", ") === "1$2, 1$6, 1$9, 2$1, 2$4" 15 | } 16 | }, 17 | { 18 | label: "CM9 shift(8)", 19 | function: () => { 20 | let c = new Chord("CM9", { octave: 1 }) 21 | c = c.shift(8) 22 | return c.key.join(", ") === "1$9, 2$1, 2$4, 2$8, 2$11" 23 | } 24 | }, 25 | { 26 | label: "CM9 shift(8) octave(1)", 27 | function: () => { 28 | let c = new Chord("CM9", { octave: 1 }) 29 | c = c.shift(8) 30 | c = c.octave(1) 31 | return c.key.join(", ") === "2$9, 3$1, 3$4, 3$8, 3$11" 32 | } 33 | }, 34 | { 35 | label: "CM9 shift(8) octave(1) shift(2) octave(1)", 36 | function: () => { 37 | let c = new Chord("CM9", { octave: 1 }) 38 | c = c.shift(8) 39 | c = c.octave(1) 40 | c = c.shift(2) 41 | c = c.octave(1) 42 | return c.key.join(", ") === "3$11, 4$3, 4$6, 4$10" 43 | } 44 | }, 45 | { 46 | label: "BM9", 47 | function: () => { 48 | let c = new Chord("BM9", { octave: 2 }) 49 | return c.key.join(", ") === "2$12, 3$4, 3$7, 3$11, 4$2" 50 | } 51 | }, 52 | { 53 | label: "BM9 shift(-2)", 54 | function: () => { 55 | let c = new Chord("BM9", { octave: 2 }) 56 | c = c.shift(-2) 57 | return c.key.join(", ") === "2$10, 3$2, 3$5, 3$9, 3$12" 58 | } 59 | }, 60 | { 61 | label: "BM9 shift(-2) shift(-5)", 62 | function: () => { 63 | let c = new Chord("BM9", { octave: 2 }) 64 | c = c.shift(-2) 65 | c = c.shift(-5) 66 | return c.key.join(", ") === "2$5, 2$9, 2$12, 3$4, 3$7" 67 | } 68 | }, 69 | { 70 | label: "BM9 shift(-2) shift(-5) octave(-2)", 71 | function: () => { 72 | let c = new Chord("BM9", { octave: 2 }) 73 | c = c.shift(-2) 74 | c = c.shift(-5) 75 | c = c.octave(-2) 76 | return c.key.join(", ") === "1$4, 1$7" 77 | } 78 | }, 79 | { 80 | label: "D#M7", 81 | function: () => { 82 | let c = new Chord("D#M7") 83 | return c.key.join(", ") === "2$4, 2$8, 2$11, 3$3" 84 | } 85 | }, 86 | { 87 | label: "D#M7 shift(0)", 88 | function: () => { 89 | let c = new Chord("D#M7") 90 | c = c.shift(0) 91 | return c.key.join(", ") === "2$4, 2$8, 2$11, 3$3" 92 | } 93 | }, 94 | { 95 | label: "D#M7 octave(0)", 96 | function: () => { 97 | let c = new Chord("D#M7") 98 | c = c.octave(0) 99 | return c.key.join(", ") === "2$4, 2$8, 2$11, 3$3" 100 | } 101 | }, 102 | { 103 | label: "E", 104 | function: () => { 105 | let c = new Chord("E") 106 | return c.key.join(", ") === "2$5, 2$9, 2$12" 107 | } 108 | }, 109 | { 110 | label: "E rotate()", 111 | function: () => { 112 | let c = new Chord("E") 113 | c = c.rotate() 114 | return c.key.join(", ") === "2$9, 2$12, 3$5" 115 | } 116 | }, 117 | { 118 | label: "E rotate().rotate()", 119 | function: () => { 120 | let c = new Chord("E") 121 | c = c.rotate().rotate() 122 | return c.key.join(", ") === "2$12, 3$5, 3$9" 123 | } 124 | }, 125 | { 126 | label: "G6", 127 | function: () => { 128 | let c = new Chord("G6") 129 | return c.key.join(", ") === "2$8, 2$12, 3$3, 3$5" 130 | } 131 | }, 132 | { 133 | label: "G6 slice(2)", 134 | function: () => { 135 | let c = new Chord("G6") 136 | c = c.slice(2) 137 | return c.key.join(", ") === "3$3, 3$5" 138 | } 139 | }, 140 | { 141 | label: "G6 slice(0,1)", 142 | function: () => { 143 | let c = new Chord("G6") 144 | c = c.slice(0,1) 145 | return c.key.join(", ") === "2$8" 146 | } 147 | }, 148 | { 149 | label: "G6 slice(-3)", 150 | function: () => { 151 | let c = new Chord("G6") 152 | c = c.slice(-3) 153 | return c.key.join(", ") === "2$12, 3$3, 3$5" 154 | } 155 | } 156 | ] -------------------------------------------------------------------------------- /test/execute.entry.js: -------------------------------------------------------------------------------- 1 | import Ongaq from "../src/api" 2 | import cases from "./cases" 3 | 4 | cases.forEach((c,index)=>{ 5 | describe("chord",()=> { 6 | it(c.label || index,()=> { 7 | const result = c.function() 8 | if(!result) console.error(c) 9 | expect(result).toBe(true) 10 | }) 11 | }) 12 | }) -------------------------------------------------------------------------------- /test/test.webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: [ 5 | './test/execute.entry.js' 6 | ], 7 | output: { 8 | path: path.resolve('./test'), 9 | filename: 'test.bundle.js' 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.js$/, 15 | use: [ 16 | { 17 | loader: 'babel-loader', 18 | options: { 19 | presets: [ 20 | ['env', {'modules': false}] 21 | ] 22 | } 23 | } 24 | ], 25 | exclude: /node_modules/, 26 | } 27 | ] 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | require('webpack'); 2 | require('babel-core/register'); 3 | const TerserPlugin = require("terser-webpack-plugin"); 4 | const path = require('path'); 5 | 6 | module.exports = { 7 | entry: { 8 | 'build/ongaq': './src/api.js' 9 | }, 10 | output: { 11 | path: path.resolve('./') 12 | }, 13 | optimization: { 14 | minimize: true, 15 | minimizer: [new TerserPlugin()] 16 | }, 17 | mode: "production" 18 | } 19 | --------------------------------------------------------------------------------