├── sample
├── script.js
├── wall.png
├── script.example.js
└── index.html
├── .babelrc
├── src
├── Constants
│ ├── VERSION.js
│ ├── ENDPOINT.js
│ ├── ROOT.js
│ ├── DRUM_NOTE.js
│ └── SCHEME.js
├── Ongaq
│ ├── module
│ │ ├── pool.pan.js
│ │ ├── pool.delayfunction.js
│ │ ├── pool.panfunction.js
│ │ ├── isDrumNoteName.js
│ │ ├── toDrumNoteName.js
│ │ ├── pool.gain.js
│ │ ├── pool.element.js
│ │ ├── defaults.js
│ │ ├── isActive.js
│ │ ├── DictPool.js
│ │ ├── make.js
│ │ ├── toPianoNoteName.js
│ │ ├── make
│ │ │ ├── makeDelay.js
│ │ │ ├── makePanner.js
│ │ │ └── makeAudioBuffer.js
│ │ ├── Cacher.js
│ │ ├── Pool.js
│ │ ├── inspect.js
│ │ ├── pool.delay.js
│ │ ├── AudioCore.js
│ │ ├── Helper.js
│ │ ├── BufferYard.js
│ │ └── Part.js
│ ├── plugin
│ │ └── filtermapper
│ │ │ ├── PRIORITY.js
│ │ │ ├── index.js
│ │ │ ├── empty.js
│ │ │ ├── phrase.js
│ │ │ ├── pan.js
│ │ │ ├── arpeggio.js
│ │ │ └── note.js
│ └── Ongaq.js
├── Helper
│ ├── Filter.js
│ ├── shiftKeys.js
│ └── Chord.js
└── api.js
├── .gitignore
├── karma.conf.js
├── test
├── execute.entry.js
├── test.webpack.config.js
└── cases.js
├── .editorconfig
├── webpack.config.js
├── readme.md
├── .eslintrc.json
├── .circleci
└── config.yml
└── package.json
/sample/script.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["env"]
3 | }
4 |
--------------------------------------------------------------------------------
/src/Constants/VERSION.js:
--------------------------------------------------------------------------------
1 | export default "1.5.0"
2 |
--------------------------------------------------------------------------------
/sample/wall.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeninth/ongaq-js/HEAD/sample/wall.png
--------------------------------------------------------------------------------
/src/Constants/ENDPOINT.js:
--------------------------------------------------------------------------------
1 | const ENDPOINT = "https://api.ongaqjs.com"
2 | export default ENDPOINT
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .sass-cache
3 | thumbs.db
4 | node_modules
5 | *.log
6 | yarn.lock
7 | test/test.bundle.js
--------------------------------------------------------------------------------
/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.delayfunction.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/isDrumNoteName.js:
--------------------------------------------------------------------------------
1 | import DRUM_NOTE from "../../Constants/DRUM_NOTE"
2 |
3 | export default (raw = "") => !!DRUM_NOTE.get(raw)
--------------------------------------------------------------------------------
/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/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/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.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | })
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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/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/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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/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/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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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
--------------------------------------------------------------------------------
/sample/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Ongaq JS Sample
8 |
98 |
99 |
100 |
101 | Ongaq JS Sample
102 |
105 |
106 |
110 |
111 |
112 |
113 |
114 |
115 |
--------------------------------------------------------------------------------
/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/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/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/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
--------------------------------------------------------------------------------
/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 | ]
--------------------------------------------------------------------------------
/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/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/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/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
--------------------------------------------------------------------------------