misairu 4.1.0 → 5.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintignore +3 -0
- package/.eslintrc.js +18 -0
- package/.prettierignore +2 -0
- package/.prettierrc.json +8 -0
- package/LICENSE +20 -20
- package/README.md +144 -115
- package/dist/index.d.ts +2 -0
- package/dist/misairu.d.ts +161 -0
- package/dist/misairu.iife.js +2 -0
- package/dist/misairu.iife.js.map +1 -0
- package/dist/misairu.js +2 -0
- package/dist/misairu.js.map +1 -0
- package/dist/misairu.modern.js +2 -0
- package/dist/misairu.modern.js.map +1 -0
- package/dist/misairu.module.js +2 -0
- package/dist/misairu.module.js.map +1 -0
- package/dist/misairu.umd.js +2 -0
- package/dist/misairu.umd.js.map +1 -0
- package/dist/processors/repeat.d.ts +6 -0
- package/dist/types.d.ts +46 -0
- package/dist/utilities/audio.d.ts +11 -0
- package/dist/utilities/source.d.ts +15 -0
- package/package.json +52 -26
- package/src/index.ts +2 -0
- package/src/misairu.ts +308 -0
- package/src/processors/repeat.ts +35 -0
- package/src/types.ts +58 -0
- package/src/utilities/audio.ts +22 -0
- package/src/utilities/source.ts +28 -0
- package/tsconfig.json +11 -0
- package/misairu.js +0 -255
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"misairu.modern.js","sources":["../src/processors/repeat.ts","../src/utilities/audio.ts","../src/misairu.ts","../src/utilities/source.ts"],"sourcesContent":["import { EventFunction, EventTrack, ITrackProcessor } from \"../types\";\r\n\r\nconst REPEAT_NAME_REGEX = new RegExp(/repeat:\\d:\\d:\\d/)\r\n\r\nexport class RepeatTrackProcessor implements ITrackProcessor {\r\n deleteOriginTrack = true;\r\n\r\n matches(name: string): boolean {\r\n return name.match(REPEAT_NAME_REGEX) !== null\r\n }\r\n\r\n process(name: string, track: EventFunction): [string, EventTrack] {\r\n if (typeof track != 'function')\r\n throw Error(`The value of repeat track \"${track}\" is not a function`)\r\n\r\n const repeatTrackArgs = name.split(':')\r\n\r\n if (repeatTrackArgs.length != 4)\r\n throw Error(`The repeat track \"${name}\" does not supply the valid amount of arguments`)\r\n\r\n const startTime = parseFloat(repeatTrackArgs[1])\r\n const interval = parseFloat(repeatTrackArgs[2])\r\n const endTime = parseFloat(repeatTrackArgs[3])\r\n\r\n let time = startTime\r\n const tempTrack = {}\r\n\r\n do {\r\n tempTrack[time.toString()] = track\r\n time += interval\r\n } while (time < endTime)\r\n\r\n return [`repeat-${Math.random().toString(36).substring(7)}`, tempTrack]\r\n }\r\n}","/**\r\n * Method to turn the passed decibel values into volume values for the audio playback\r\n *\r\n * @param db decibel value\r\n * @returns volume value\r\n */\r\nexport function dbToVolume(db: number): number {\r\n return Math.pow(10, db / 20)\r\n}\r\n\r\n/**\r\n * Clamps the volume to a min/max value to prevent accidental oversetting to way too loud measures\r\n */\r\nexport function clampGain(volume: number): number {\r\n if (volume < -80) {\r\n return -80\r\n } else if (volume > 5) {\r\n return 5\r\n }\r\n\r\n return volume\r\n}","import { RepeatTrackProcessor } from './processors/repeat'\r\nimport { EventCache, ITrackProcessor, TimingObject } from './types'\r\nimport { dbToVolume, clampGain } from './utilities/audio'\r\nimport { fetchAudioSource, attachAudioElementSource } from './utilities/source'\r\n\r\n/**\r\n * Main misairu class\r\n */\r\nexport class Misairu {\r\n /**\r\n * The HTML5 AudioContext used for accurately timing our events\r\n */\r\n private readonly _audioContext: AudioContext\r\n\r\n /**\r\n * A source node to play our audio from, either a buffered source from a downloaded media file or a media element source\r\n *\r\n * @default null\r\n */\r\n private _audioSource: HTMLMediaElement | AudioBufferSourceNode | MediaElementAudioSourceNode | null = null\r\n\r\n /**\r\n * A object containing the last executed time key per track to not execute an event on every tick\r\n *\r\n * @default {}\r\n */\r\n private _cache: EventCache = {}\r\n\r\n /**\r\n * Reference to the `requestAnimationFrame` handler\r\n *\r\n * @default null\r\n */\r\n private _eventHandler: number | null = null\r\n\r\n /**\r\n * Gain node from our audio context to control audio volume\r\n */\r\n private readonly _gainNode: GainNode\r\n\r\n /**\r\n * Boolean value describing if this instance is currently muted\r\n */\r\n private _muted = false\r\n\r\n /**\r\n * Boolean value describing if this instance is currently paused\r\n */\r\n private _paused = false\r\n\r\n /**\r\n * The time when event handling was started, based on the audio contexts `currentTime` when `start()` was called\r\n */\r\n private _startTime = 0\r\n\r\n /**\r\n * Object containing all timing tracks and events to be executed\r\n */\r\n private _timings: TimingObject\r\n\r\n /**\r\n * Volume of the current instance\r\n */\r\n private _volume = 0\r\n\r\n /**\r\n * List of (predefined) track processors\r\n */\r\n private _processors: ITrackProcessor[] = [\r\n new RepeatTrackProcessor()\r\n ]\r\n\r\n get volume(): number {\r\n return this._volume\r\n }\r\n\r\n set volume(db: number) {\r\n this._volume = clampGain(db)\r\n\r\n if (!this._muted) {\r\n this._gainNode.gain.value = dbToVolume(this._volume)\r\n }\r\n }\r\n\r\n /**\r\n * misairu constructor\r\n *\r\n * @param audioSource a string or HTML element to be used as audio source\r\n * @param timings a object containing event timing information\r\n */\r\n constructor(audioSource: string | HTMLMediaElement, timings: TimingObject) {\r\n if (timings === null) console.error('You need to specify a timings object')\r\n this._timings = timings\r\n\r\n this._audioContext = new AudioContext()\r\n\r\n this._gainNode = this._audioContext.createGain()\r\n this._gainNode.connect(this._audioContext.destination)\r\n\r\n this.getOptimalAudioSource(audioSource)\r\n\r\n this.processTracks()\r\n }\r\n\r\n /**\r\n * Method to figure out the best course of action to take with the passed audio source\r\n *\r\n * @param audioSource the audio source `Misairu` has been constructed with\r\n * @internal\r\n */\r\n private getOptimalAudioSource(audioSource: string | HTMLMediaElement): void {\r\n if (typeof audioSource == 'string') {\r\n fetchAudioSource(audioSource, this._audioContext).then((audioSource) => {\r\n this._audioSource = audioSource\r\n this._audioSource.connect(this._gainNode)\r\n\r\n document.dispatchEvent(new Event('misairu.ready'))\r\n })\r\n } else if (\r\n typeof audioSource == 'object' &&\r\n (audioSource.tagName == 'AUDIO' || audioSource.tagName == 'VIDEO')\r\n ) {\r\n this._audioSource = attachAudioElementSource(audioSource, this._audioContext)\r\n this._audioSource.connect(this._gainNode)\r\n }\r\n }\r\n\r\n /**\r\n * Mutes the instance audio\r\n *\r\n * @public\r\n */\r\n public mute(): void {\r\n if (!this._muted) {\r\n this._muted = true\r\n this._gainNode.gain.value = 0\r\n }\r\n }\r\n\r\n /**\r\n * Unmutes the instance audio\r\n *\r\n * @public\r\n */\r\n public unmute(): void {\r\n if (this._muted) {\r\n this._muted = false\r\n this._gainNode.gain.value = dbToVolume(this._volume)\r\n }\r\n }\r\n\r\n /**\r\n * Pauses instance playback\r\n *\r\n * @public\r\n */\r\n public pause(): void {\r\n if (!this._paused) {\r\n this._audioContext.suspend()\r\n this._paused = true\r\n }\r\n }\r\n\r\n /**\r\n * Resumes instance playback\r\n *\r\n * @public\r\n */\r\n public unpause(): void {\r\n if (this._paused) {\r\n this._audioContext.resume()\r\n this._paused = false\r\n }\r\n }\r\n\r\n /**\r\n * Set a cache entry for the given track\r\n *\r\n * @param track track to set a cache entry for\r\n * @param entry value of the cache entry\r\n * @internal\r\n */\r\n private setCacheEntry(track: string, entry: string): void {\r\n this._cache[track] = entry\r\n }\r\n\r\n /**\r\n * Get cache entry for the given track\r\n *\r\n * @param track track to get a cache entry for\r\n * @returns a cache entry\r\n * @internal\r\n */\r\n private getCacheEntry(track: string): string {\r\n return this._cache[track]\r\n }\r\n\r\n /**\r\n * Get all tracks from the timing configuration\r\n *\r\n * @returns a list of all track names\r\n * @internal\r\n */\r\n private getAllTracks(): string[] {\r\n return Object.keys(this._timings)\r\n }\r\n\r\n /**\r\n * Returns the current active timing key for a given track\r\n *\r\n * @param track track to get the timing key from\r\n * @param currentTime current playback time\r\n * @returns the current active timing key\r\n * @internal\r\n */\r\n private getActiveTimingKey(track: string, currentTime: number): string {\r\n const timingKeys = Object.keys(this._timings[track])\r\n\r\n const activeTimings = timingKeys.filter((timing) => {\r\n if (currentTime >= parseFloat(timing)) {\r\n return true\r\n }\r\n })\r\n\r\n return activeTimings[activeTimings.length - 1]\r\n }\r\n\r\n /**\r\n * Main method to process special sections in the timing configuration\r\n *\r\n * @internal\r\n */\r\n private processTracks(): void {\r\n this.getAllTracks().forEach((trackName) => {\r\n this._processors.forEach((processor: ITrackProcessor) => {\r\n if (processor.matches(trackName)) {\r\n const [processedTrackName, eventTrack] = processor.process(trackName, this._timings[trackName])\r\n\r\n this._timings[processedTrackName] = eventTrack\r\n\r\n if (processor.deleteOriginTrack) {\r\n delete this._timings[trackName]\r\n }\r\n }\r\n })\r\n })\r\n }\r\n\r\n /**\r\n * Start audio playback and event handling\r\n *\r\n * @public\r\n */\r\n public start(): void {\r\n this._startTime = this._audioContext.currentTime\r\n\r\n ;(this._audioSource as AudioBufferSourceNode).start()\r\n\r\n this.startEventHandling()\r\n }\r\n\r\n /**\r\n * Method to start event handler loop\r\n *\r\n * @internal\r\n */\r\n private startEventHandling(): void {\r\n if (this._eventHandler == null) {\r\n this._eventHandler = window.requestAnimationFrame(() => {\r\n this.handleEvents()\r\n })\r\n }\r\n }\r\n\r\n /**\r\n * Event handler method, is running in a loop using `requestAnimationFrame`\r\n *\r\n * @internal\r\n */\r\n private handleEvents(): void {\r\n const time = this._audioContext.currentTime - this._startTime\r\n\r\n this.getAllTracks().forEach((track) => {\r\n const timingKey = this.getActiveTimingKey(track, time)\r\n\r\n if (timingKey !== null && !(this.getCacheEntry(track) == timingKey)) {\r\n this.executeEvent(track, timingKey, time)\r\n this.setCacheEntry(track, timingKey)\r\n }\r\n })\r\n\r\n this._eventHandler = window.requestAnimationFrame(() => {\r\n this.handleEvents()\r\n })\r\n }\r\n\r\n /**\r\n * Method to execute the event for a given timing key on a given track\r\n *\r\n * @param track the track to execute the event on\r\n * @param timingKey the timing key to execute\r\n * @param time current playback time\r\n * @internal\r\n */\r\n private executeEvent(track: string, timingKey: string, time: number): void {\r\n this._timings[track][timingKey](this, timingKey, track, time)\r\n }\r\n}\r\n","/**\r\n * Method to fetch the external audio file (if the audio source parameter was a string)\r\n * and turning it into a `AudioBufferSourceNode`\r\n *\r\n * @param audioSource the audio source `Misairu` has been constructed with\r\n * @internal\r\n */\r\nexport async function fetchAudioSource(audioSource: string, audioContext: AudioContext): Promise<AudioBufferSourceNode> {\r\n const source = audioContext.createBufferSource()\r\n\r\n const response = await fetch(audioSource)\r\n const arrayBuffer = await response.arrayBuffer()\r\n const buffer = await audioContext.decodeAudioData(arrayBuffer)\r\n\r\n source.buffer = buffer\r\n\r\n return source\r\n}\r\n\r\n/**\r\n * Method to get an `MediaElementAudioSourceNode` from the passed audio source\r\n *\r\n * @param audioSource the audio source `Misairu` has been constructed with\r\n * @internal\r\n */\r\nexport function attachAudioElementSource(audioSource: HTMLMediaElement, audioContext: AudioContext): MediaElementAudioSourceNode {\r\n return audioContext.createMediaElementSource(audioSource)\r\n}"],"names":["REPEAT_NAME_REGEX","RegExp","RepeatTrackProcessor","deleteOriginTrack","matches","name","match","process","track","Error","repeatTrackArgs","split","length","startTime","parseFloat","interval","endTime","time","tempTrack","toString","Math","random","substring","dbToVolume","db","pow","Misairu","volume","_volume","this","_muted","_gainNode","gain","value","constructor","audioSource","timings","_audioContext","_audioSource","_cache","_eventHandler","_paused","_startTime","_timings","_processors","console","error","AudioContext","createGain","connect","destination","getOptimalAudioSource","processTracks","audioContext","source","createBufferSource","response","fetch","arrayBuffer","buffer","decodeAudioData","fetchAudioSource","then","document","dispatchEvent","Event","tagName","createMediaElementSource","attachAudioElementSource","mute","unmute","pause","suspend","unpause","resume","setCacheEntry","entry","getCacheEntry","getAllTracks","Object","keys","getActiveTimingKey","currentTime","activeTimings","filter","timing","forEach","trackName","processor","processedTrackName","eventTrack","start","startEventHandling","window","requestAnimationFrame","handleEvents","timingKey","executeEvent"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var t=new RegExp(/repeat:\d:\d:\d/),e=function(){function e(){this.deleteOriginTrack=!0}var n=e.prototype;return n.matches=function(e){return null!==e.match(t)},n.process=function(t,e){if("function"!=typeof e)throw Error('The value of repeat track "'+e+'" is not a function');var n=t.split(":");if(4!=n.length)throw Error('The repeat track "'+t+'" does not supply the valid amount of arguments');var i=parseFloat(n[1]),o=parseFloat(n[2]),r=parseFloat(n[3]),s=i,a={};do{a[s.toString()]=e,s+=o}while(s<r);return["repeat-"+Math.random().toString(36).substring(7),a]},e}();function n(t){return Math.pow(10,t/20)}var i=function(){function t(t,n){this._audioContext=void 0,this._audioSource=null,this._cache={},this._eventHandler=null,this._gainNode=void 0,this._muted=!1,this._paused=!1,this._startTime=0,this._timings=void 0,this._volume=0,this._processors=[new e],null===n&&console.error("You need to specify a timings object"),this._timings=n,this._audioContext=new AudioContext,this._gainNode=this._audioContext.createGain(),this._gainNode.connect(this._audioContext.destination),this.getOptimalAudioSource(t),this.processTracks()}var i,o=t.prototype;return o.getOptimalAudioSource=function(t){var e=this;"string"==typeof t?function(t,e){try{var n=e.createBufferSource();return Promise.resolve(fetch(t)).then(function(t){return Promise.resolve(t.arrayBuffer()).then(function(t){return Promise.resolve(e.decodeAudioData(t)).then(function(t){return n.buffer=t,n})})})}catch(t){return Promise.reject(t)}}(t,this._audioContext).then(function(t){e._audioSource=t,e._audioSource.connect(e._gainNode),document.dispatchEvent(new Event("misairu.ready"))}):"object"!=typeof t||"AUDIO"!=t.tagName&&"VIDEO"!=t.tagName||(this._audioSource=function(t,e){return e.createMediaElementSource(t)}(t,this._audioContext),this._audioSource.connect(this._gainNode))},o.mute=function(){this._muted||(this._muted=!0,this._gainNode.gain.value=0)},o.unmute=function(){this._muted&&(this._muted=!1,this._gainNode.gain.value=n(this._volume))},o.pause=function(){this._paused||(this._audioContext.suspend(),this._paused=!0)},o.unpause=function(){this._paused&&(this._audioContext.resume(),this._paused=!1)},o.setCacheEntry=function(t,e){this._cache[t]=e},o.getCacheEntry=function(t){return this._cache[t]},o.getAllTracks=function(){return Object.keys(this._timings)},o.getActiveTimingKey=function(t,e){var n=Object.keys(this._timings[t]).filter(function(t){if(e>=parseFloat(t))return!0});return n[n.length-1]},o.processTracks=function(){var t=this;this.getAllTracks().forEach(function(e){t._processors.forEach(function(n){if(n.matches(e)){var i=n.process(e,t._timings[e]);t._timings[i[0]]=i[1],n.deleteOriginTrack&&delete t._timings[e]}})})},o.start=function(){this._startTime=this._audioContext.currentTime,this._audioSource.start(),this.startEventHandling()},o.startEventHandling=function(){var t=this;null==this._eventHandler&&(this._eventHandler=window.requestAnimationFrame(function(){t.handleEvents()}))},o.handleEvents=function(){var t=this,e=this._audioContext.currentTime-this._startTime;this.getAllTracks().forEach(function(n){var i=t.getActiveTimingKey(n,e);null!==i&&t.getCacheEntry(n)!=i&&(t.executeEvent(n,i,e),t.setCacheEntry(n,i))}),this._eventHandler=window.requestAnimationFrame(function(){t.handleEvents()})},o.executeEvent=function(t,e,n){this._timings[t][e](this,e,t,n)},(i=[{key:"volume",get:function(){return this._volume},set:function(t){var e;this._volume=(e=t)<-80?-80:e>5?5:e,this._muted||(this._gainNode.gain.value=n(this._volume))}}])&&function(t,e){for(var n=0;n<e.length;n++){var i=e[n];i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(t,i.key,i)}}(t.prototype,i),t}();export{i as default};
|
|
2
|
+
//# sourceMappingURL=misairu.module.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"misairu.module.js","sources":["../src/processors/repeat.ts","../src/utilities/audio.ts","../src/misairu.ts","../src/utilities/source.ts"],"sourcesContent":["import { EventFunction, EventTrack, ITrackProcessor } from \"../types\";\r\n\r\nconst REPEAT_NAME_REGEX = new RegExp(/repeat:\\d:\\d:\\d/)\r\n\r\nexport class RepeatTrackProcessor implements ITrackProcessor {\r\n deleteOriginTrack = true;\r\n\r\n matches(name: string): boolean {\r\n return name.match(REPEAT_NAME_REGEX) !== null\r\n }\r\n\r\n process(name: string, track: EventFunction): [string, EventTrack] {\r\n if (typeof track != 'function')\r\n throw Error(`The value of repeat track \"${track}\" is not a function`)\r\n\r\n const repeatTrackArgs = name.split(':')\r\n\r\n if (repeatTrackArgs.length != 4)\r\n throw Error(`The repeat track \"${name}\" does not supply the valid amount of arguments`)\r\n\r\n const startTime = parseFloat(repeatTrackArgs[1])\r\n const interval = parseFloat(repeatTrackArgs[2])\r\n const endTime = parseFloat(repeatTrackArgs[3])\r\n\r\n let time = startTime\r\n const tempTrack = {}\r\n\r\n do {\r\n tempTrack[time.toString()] = track\r\n time += interval\r\n } while (time < endTime)\r\n\r\n return [`repeat-${Math.random().toString(36).substring(7)}`, tempTrack]\r\n }\r\n}","/**\r\n * Method to turn the passed decibel values into volume values for the audio playback\r\n *\r\n * @param db decibel value\r\n * @returns volume value\r\n */\r\nexport function dbToVolume(db: number): number {\r\n return Math.pow(10, db / 20)\r\n}\r\n\r\n/**\r\n * Clamps the volume to a min/max value to prevent accidental oversetting to way too loud measures\r\n */\r\nexport function clampGain(volume: number): number {\r\n if (volume < -80) {\r\n return -80\r\n } else if (volume > 5) {\r\n return 5\r\n }\r\n\r\n return volume\r\n}","import { RepeatTrackProcessor } from './processors/repeat'\r\nimport { EventCache, ITrackProcessor, TimingObject } from './types'\r\nimport { dbToVolume, clampGain } from './utilities/audio'\r\nimport { fetchAudioSource, attachAudioElementSource } from './utilities/source'\r\n\r\n/**\r\n * Main misairu class\r\n */\r\nexport class Misairu {\r\n /**\r\n * The HTML5 AudioContext used for accurately timing our events\r\n */\r\n private readonly _audioContext: AudioContext\r\n\r\n /**\r\n * A source node to play our audio from, either a buffered source from a downloaded media file or a media element source\r\n *\r\n * @default null\r\n */\r\n private _audioSource: HTMLMediaElement | AudioBufferSourceNode | MediaElementAudioSourceNode | null = null\r\n\r\n /**\r\n * A object containing the last executed time key per track to not execute an event on every tick\r\n *\r\n * @default {}\r\n */\r\n private _cache: EventCache = {}\r\n\r\n /**\r\n * Reference to the `requestAnimationFrame` handler\r\n *\r\n * @default null\r\n */\r\n private _eventHandler: number | null = null\r\n\r\n /**\r\n * Gain node from our audio context to control audio volume\r\n */\r\n private readonly _gainNode: GainNode\r\n\r\n /**\r\n * Boolean value describing if this instance is currently muted\r\n */\r\n private _muted = false\r\n\r\n /**\r\n * Boolean value describing if this instance is currently paused\r\n */\r\n private _paused = false\r\n\r\n /**\r\n * The time when event handling was started, based on the audio contexts `currentTime` when `start()` was called\r\n */\r\n private _startTime = 0\r\n\r\n /**\r\n * Object containing all timing tracks and events to be executed\r\n */\r\n private _timings: TimingObject\r\n\r\n /**\r\n * Volume of the current instance\r\n */\r\n private _volume = 0\r\n\r\n /**\r\n * List of (predefined) track processors\r\n */\r\n private _processors: ITrackProcessor[] = [\r\n new RepeatTrackProcessor()\r\n ]\r\n\r\n get volume(): number {\r\n return this._volume\r\n }\r\n\r\n set volume(db: number) {\r\n this._volume = clampGain(db)\r\n\r\n if (!this._muted) {\r\n this._gainNode.gain.value = dbToVolume(this._volume)\r\n }\r\n }\r\n\r\n /**\r\n * misairu constructor\r\n *\r\n * @param audioSource a string or HTML element to be used as audio source\r\n * @param timings a object containing event timing information\r\n */\r\n constructor(audioSource: string | HTMLMediaElement, timings: TimingObject) {\r\n if (timings === null) console.error('You need to specify a timings object')\r\n this._timings = timings\r\n\r\n this._audioContext = new AudioContext()\r\n\r\n this._gainNode = this._audioContext.createGain()\r\n this._gainNode.connect(this._audioContext.destination)\r\n\r\n this.getOptimalAudioSource(audioSource)\r\n\r\n this.processTracks()\r\n }\r\n\r\n /**\r\n * Method to figure out the best course of action to take with the passed audio source\r\n *\r\n * @param audioSource the audio source `Misairu` has been constructed with\r\n * @internal\r\n */\r\n private getOptimalAudioSource(audioSource: string | HTMLMediaElement): void {\r\n if (typeof audioSource == 'string') {\r\n fetchAudioSource(audioSource, this._audioContext).then((audioSource) => {\r\n this._audioSource = audioSource\r\n this._audioSource.connect(this._gainNode)\r\n\r\n document.dispatchEvent(new Event('misairu.ready'))\r\n })\r\n } else if (\r\n typeof audioSource == 'object' &&\r\n (audioSource.tagName == 'AUDIO' || audioSource.tagName == 'VIDEO')\r\n ) {\r\n this._audioSource = attachAudioElementSource(audioSource, this._audioContext)\r\n this._audioSource.connect(this._gainNode)\r\n }\r\n }\r\n\r\n /**\r\n * Mutes the instance audio\r\n *\r\n * @public\r\n */\r\n public mute(): void {\r\n if (!this._muted) {\r\n this._muted = true\r\n this._gainNode.gain.value = 0\r\n }\r\n }\r\n\r\n /**\r\n * Unmutes the instance audio\r\n *\r\n * @public\r\n */\r\n public unmute(): void {\r\n if (this._muted) {\r\n this._muted = false\r\n this._gainNode.gain.value = dbToVolume(this._volume)\r\n }\r\n }\r\n\r\n /**\r\n * Pauses instance playback\r\n *\r\n * @public\r\n */\r\n public pause(): void {\r\n if (!this._paused) {\r\n this._audioContext.suspend()\r\n this._paused = true\r\n }\r\n }\r\n\r\n /**\r\n * Resumes instance playback\r\n *\r\n * @public\r\n */\r\n public unpause(): void {\r\n if (this._paused) {\r\n this._audioContext.resume()\r\n this._paused = false\r\n }\r\n }\r\n\r\n /**\r\n * Set a cache entry for the given track\r\n *\r\n * @param track track to set a cache entry for\r\n * @param entry value of the cache entry\r\n * @internal\r\n */\r\n private setCacheEntry(track: string, entry: string): void {\r\n this._cache[track] = entry\r\n }\r\n\r\n /**\r\n * Get cache entry for the given track\r\n *\r\n * @param track track to get a cache entry for\r\n * @returns a cache entry\r\n * @internal\r\n */\r\n private getCacheEntry(track: string): string {\r\n return this._cache[track]\r\n }\r\n\r\n /**\r\n * Get all tracks from the timing configuration\r\n *\r\n * @returns a list of all track names\r\n * @internal\r\n */\r\n private getAllTracks(): string[] {\r\n return Object.keys(this._timings)\r\n }\r\n\r\n /**\r\n * Returns the current active timing key for a given track\r\n *\r\n * @param track track to get the timing key from\r\n * @param currentTime current playback time\r\n * @returns the current active timing key\r\n * @internal\r\n */\r\n private getActiveTimingKey(track: string, currentTime: number): string {\r\n const timingKeys = Object.keys(this._timings[track])\r\n\r\n const activeTimings = timingKeys.filter((timing) => {\r\n if (currentTime >= parseFloat(timing)) {\r\n return true\r\n }\r\n })\r\n\r\n return activeTimings[activeTimings.length - 1]\r\n }\r\n\r\n /**\r\n * Main method to process special sections in the timing configuration\r\n *\r\n * @internal\r\n */\r\n private processTracks(): void {\r\n this.getAllTracks().forEach((trackName) => {\r\n this._processors.forEach((processor: ITrackProcessor) => {\r\n if (processor.matches(trackName)) {\r\n const [processedTrackName, eventTrack] = processor.process(trackName, this._timings[trackName])\r\n\r\n this._timings[processedTrackName] = eventTrack\r\n\r\n if (processor.deleteOriginTrack) {\r\n delete this._timings[trackName]\r\n }\r\n }\r\n })\r\n })\r\n }\r\n\r\n /**\r\n * Start audio playback and event handling\r\n *\r\n * @public\r\n */\r\n public start(): void {\r\n this._startTime = this._audioContext.currentTime\r\n\r\n ;(this._audioSource as AudioBufferSourceNode).start()\r\n\r\n this.startEventHandling()\r\n }\r\n\r\n /**\r\n * Method to start event handler loop\r\n *\r\n * @internal\r\n */\r\n private startEventHandling(): void {\r\n if (this._eventHandler == null) {\r\n this._eventHandler = window.requestAnimationFrame(() => {\r\n this.handleEvents()\r\n })\r\n }\r\n }\r\n\r\n /**\r\n * Event handler method, is running in a loop using `requestAnimationFrame`\r\n *\r\n * @internal\r\n */\r\n private handleEvents(): void {\r\n const time = this._audioContext.currentTime - this._startTime\r\n\r\n this.getAllTracks().forEach((track) => {\r\n const timingKey = this.getActiveTimingKey(track, time)\r\n\r\n if (timingKey !== null && !(this.getCacheEntry(track) == timingKey)) {\r\n this.executeEvent(track, timingKey, time)\r\n this.setCacheEntry(track, timingKey)\r\n }\r\n })\r\n\r\n this._eventHandler = window.requestAnimationFrame(() => {\r\n this.handleEvents()\r\n })\r\n }\r\n\r\n /**\r\n * Method to execute the event for a given timing key on a given track\r\n *\r\n * @param track the track to execute the event on\r\n * @param timingKey the timing key to execute\r\n * @param time current playback time\r\n * @internal\r\n */\r\n private executeEvent(track: string, timingKey: string, time: number): void {\r\n this._timings[track][timingKey](this, timingKey, track, time)\r\n }\r\n}\r\n","/**\r\n * Method to fetch the external audio file (if the audio source parameter was a string)\r\n * and turning it into a `AudioBufferSourceNode`\r\n *\r\n * @param audioSource the audio source `Misairu` has been constructed with\r\n * @internal\r\n */\r\nexport async function fetchAudioSource(audioSource: string, audioContext: AudioContext): Promise<AudioBufferSourceNode> {\r\n const source = audioContext.createBufferSource()\r\n\r\n const response = await fetch(audioSource)\r\n const arrayBuffer = await response.arrayBuffer()\r\n const buffer = await audioContext.decodeAudioData(arrayBuffer)\r\n\r\n source.buffer = buffer\r\n\r\n return source\r\n}\r\n\r\n/**\r\n * Method to get an `MediaElementAudioSourceNode` from the passed audio source\r\n *\r\n * @param audioSource the audio source `Misairu` has been constructed with\r\n * @internal\r\n */\r\nexport function attachAudioElementSource(audioSource: HTMLMediaElement, audioContext: AudioContext): MediaElementAudioSourceNode {\r\n return audioContext.createMediaElementSource(audioSource)\r\n}"],"names":["REPEAT_NAME_REGEX","RegExp","RepeatTrackProcessor","deleteOriginTrack","matches","name","match","process","track","Error","repeatTrackArgs","split","length","startTime","parseFloat","interval","endTime","time","tempTrack","toString","Math","random","substring","dbToVolume","db","pow","Misairu","audioSource","timings","_audioContext","_audioSource","_cache","_eventHandler","_gainNode","_muted","_paused","_startTime","_timings","_volume","_processors","console","error","this","AudioContext","createGain","connect","destination","getOptimalAudioSource","processTracks","audioContext","source","createBufferSource","fetch","response","arrayBuffer","decodeAudioData","buffer","fetchAudioSource","then","_this","document","dispatchEvent","Event","tagName","createMediaElementSource","attachAudioElementSource","mute","gain","value","unmute","pause","suspend","unpause","resume","setCacheEntry","entry","getCacheEntry","getAllTracks","Object","keys","getActiveTimingKey","currentTime","activeTimings","filter","timing","forEach","trackName","_this2","processor","start","startEventHandling","window","requestAnimationFrame","_this3","handleEvents","timingKey","_this4","executeEvent","volume"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t||self).misairu=e()}(this,function(){var t=new RegExp(/repeat:\d:\d:\d/),e=function(){function e(){this.deleteOriginTrack=!0}var n=e.prototype;return n.matches=function(e){return null!==e.match(t)},n.process=function(t,e){if("function"!=typeof e)throw Error('The value of repeat track "'+e+'" is not a function');var n=t.split(":");if(4!=n.length)throw Error('The repeat track "'+t+'" does not supply the valid amount of arguments');var i=parseFloat(n[1]),o=parseFloat(n[2]),r=parseFloat(n[3]),s=i,u={};do{u[s.toString()]=e,s+=o}while(s<r);return["repeat-"+Math.random().toString(36).substring(7),u]},e}();function n(t){return Math.pow(10,t/20)}return function(){function t(t,n){this._audioContext=void 0,this._audioSource=null,this._cache={},this._eventHandler=null,this._gainNode=void 0,this._muted=!1,this._paused=!1,this._startTime=0,this._timings=void 0,this._volume=0,this._processors=[new e],null===n&&console.error("You need to specify a timings object"),this._timings=n,this._audioContext=new AudioContext,this._gainNode=this._audioContext.createGain(),this._gainNode.connect(this._audioContext.destination),this.getOptimalAudioSource(t),this.processTracks()}var i,o=t.prototype;return o.getOptimalAudioSource=function(t){var e=this;"string"==typeof t?function(t,e){try{var n=e.createBufferSource();return Promise.resolve(fetch(t)).then(function(t){return Promise.resolve(t.arrayBuffer()).then(function(t){return Promise.resolve(e.decodeAudioData(t)).then(function(t){return n.buffer=t,n})})})}catch(t){return Promise.reject(t)}}(t,this._audioContext).then(function(t){e._audioSource=t,e._audioSource.connect(e._gainNode),document.dispatchEvent(new Event("misairu.ready"))}):"object"!=typeof t||"AUDIO"!=t.tagName&&"VIDEO"!=t.tagName||(this._audioSource=function(t,e){return e.createMediaElementSource(t)}(t,this._audioContext),this._audioSource.connect(this._gainNode))},o.mute=function(){this._muted||(this._muted=!0,this._gainNode.gain.value=0)},o.unmute=function(){this._muted&&(this._muted=!1,this._gainNode.gain.value=n(this._volume))},o.pause=function(){this._paused||(this._audioContext.suspend(),this._paused=!0)},o.unpause=function(){this._paused&&(this._audioContext.resume(),this._paused=!1)},o.setCacheEntry=function(t,e){this._cache[t]=e},o.getCacheEntry=function(t){return this._cache[t]},o.getAllTracks=function(){return Object.keys(this._timings)},o.getActiveTimingKey=function(t,e){var n=Object.keys(this._timings[t]).filter(function(t){if(e>=parseFloat(t))return!0});return n[n.length-1]},o.processTracks=function(){var t=this;this.getAllTracks().forEach(function(e){t._processors.forEach(function(n){if(n.matches(e)){var i=n.process(e,t._timings[e]);t._timings[i[0]]=i[1],n.deleteOriginTrack&&delete t._timings[e]}})})},o.start=function(){this._startTime=this._audioContext.currentTime,this._audioSource.start(),this.startEventHandling()},o.startEventHandling=function(){var t=this;null==this._eventHandler&&(this._eventHandler=window.requestAnimationFrame(function(){t.handleEvents()}))},o.handleEvents=function(){var t=this,e=this._audioContext.currentTime-this._startTime;this.getAllTracks().forEach(function(n){var i=t.getActiveTimingKey(n,e);null!==i&&t.getCacheEntry(n)!=i&&(t.executeEvent(n,i,e),t.setCacheEntry(n,i))}),this._eventHandler=window.requestAnimationFrame(function(){t.handleEvents()})},o.executeEvent=function(t,e,n){this._timings[t][e](this,e,t,n)},(i=[{key:"volume",get:function(){return this._volume},set:function(t){var e;this._volume=(e=t)<-80?-80:e>5?5:e,this._muted||(this._gainNode.gain.value=n(this._volume))}}])&&function(t,e){for(var n=0;n<e.length;n++){var i=e[n];i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(t,i.key,i)}}(t.prototype,i),t}()});
|
|
2
|
+
//# sourceMappingURL=misairu.umd.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"misairu.umd.js","sources":["../src/processors/repeat.ts","../src/utilities/audio.ts","../src/misairu.ts","../src/utilities/source.ts"],"sourcesContent":["import { EventFunction, EventTrack, ITrackProcessor } from \"../types\";\r\n\r\nconst REPEAT_NAME_REGEX = new RegExp(/repeat:\\d:\\d:\\d/)\r\n\r\nexport class RepeatTrackProcessor implements ITrackProcessor {\r\n deleteOriginTrack = true;\r\n\r\n matches(name: string): boolean {\r\n return name.match(REPEAT_NAME_REGEX) !== null\r\n }\r\n\r\n process(name: string, track: EventFunction): [string, EventTrack] {\r\n if (typeof track != 'function')\r\n throw Error(`The value of repeat track \"${track}\" is not a function`)\r\n\r\n const repeatTrackArgs = name.split(':')\r\n\r\n if (repeatTrackArgs.length != 4)\r\n throw Error(`The repeat track \"${name}\" does not supply the valid amount of arguments`)\r\n\r\n const startTime = parseFloat(repeatTrackArgs[1])\r\n const interval = parseFloat(repeatTrackArgs[2])\r\n const endTime = parseFloat(repeatTrackArgs[3])\r\n\r\n let time = startTime\r\n const tempTrack = {}\r\n\r\n do {\r\n tempTrack[time.toString()] = track\r\n time += interval\r\n } while (time < endTime)\r\n\r\n return [`repeat-${Math.random().toString(36).substring(7)}`, tempTrack]\r\n }\r\n}","/**\r\n * Method to turn the passed decibel values into volume values for the audio playback\r\n *\r\n * @param db decibel value\r\n * @returns volume value\r\n */\r\nexport function dbToVolume(db: number): number {\r\n return Math.pow(10, db / 20)\r\n}\r\n\r\n/**\r\n * Clamps the volume to a min/max value to prevent accidental oversetting to way too loud measures\r\n */\r\nexport function clampGain(volume: number): number {\r\n if (volume < -80) {\r\n return -80\r\n } else if (volume > 5) {\r\n return 5\r\n }\r\n\r\n return volume\r\n}","import { RepeatTrackProcessor } from './processors/repeat'\r\nimport { EventCache, ITrackProcessor, TimingObject } from './types'\r\nimport { dbToVolume, clampGain } from './utilities/audio'\r\nimport { fetchAudioSource, attachAudioElementSource } from './utilities/source'\r\n\r\n/**\r\n * Main misairu class\r\n */\r\nexport class Misairu {\r\n /**\r\n * The HTML5 AudioContext used for accurately timing our events\r\n */\r\n private readonly _audioContext: AudioContext\r\n\r\n /**\r\n * A source node to play our audio from, either a buffered source from a downloaded media file or a media element source\r\n *\r\n * @default null\r\n */\r\n private _audioSource: HTMLMediaElement | AudioBufferSourceNode | MediaElementAudioSourceNode | null = null\r\n\r\n /**\r\n * A object containing the last executed time key per track to not execute an event on every tick\r\n *\r\n * @default {}\r\n */\r\n private _cache: EventCache = {}\r\n\r\n /**\r\n * Reference to the `requestAnimationFrame` handler\r\n *\r\n * @default null\r\n */\r\n private _eventHandler: number | null = null\r\n\r\n /**\r\n * Gain node from our audio context to control audio volume\r\n */\r\n private readonly _gainNode: GainNode\r\n\r\n /**\r\n * Boolean value describing if this instance is currently muted\r\n */\r\n private _muted = false\r\n\r\n /**\r\n * Boolean value describing if this instance is currently paused\r\n */\r\n private _paused = false\r\n\r\n /**\r\n * The time when event handling was started, based on the audio contexts `currentTime` when `start()` was called\r\n */\r\n private _startTime = 0\r\n\r\n /**\r\n * Object containing all timing tracks and events to be executed\r\n */\r\n private _timings: TimingObject\r\n\r\n /**\r\n * Volume of the current instance\r\n */\r\n private _volume = 0\r\n\r\n /**\r\n * List of (predefined) track processors\r\n */\r\n private _processors: ITrackProcessor[] = [\r\n new RepeatTrackProcessor()\r\n ]\r\n\r\n get volume(): number {\r\n return this._volume\r\n }\r\n\r\n set volume(db: number) {\r\n this._volume = clampGain(db)\r\n\r\n if (!this._muted) {\r\n this._gainNode.gain.value = dbToVolume(this._volume)\r\n }\r\n }\r\n\r\n /**\r\n * misairu constructor\r\n *\r\n * @param audioSource a string or HTML element to be used as audio source\r\n * @param timings a object containing event timing information\r\n */\r\n constructor(audioSource: string | HTMLMediaElement, timings: TimingObject) {\r\n if (timings === null) console.error('You need to specify a timings object')\r\n this._timings = timings\r\n\r\n this._audioContext = new AudioContext()\r\n\r\n this._gainNode = this._audioContext.createGain()\r\n this._gainNode.connect(this._audioContext.destination)\r\n\r\n this.getOptimalAudioSource(audioSource)\r\n\r\n this.processTracks()\r\n }\r\n\r\n /**\r\n * Method to figure out the best course of action to take with the passed audio source\r\n *\r\n * @param audioSource the audio source `Misairu` has been constructed with\r\n * @internal\r\n */\r\n private getOptimalAudioSource(audioSource: string | HTMLMediaElement): void {\r\n if (typeof audioSource == 'string') {\r\n fetchAudioSource(audioSource, this._audioContext).then((audioSource) => {\r\n this._audioSource = audioSource\r\n this._audioSource.connect(this._gainNode)\r\n\r\n document.dispatchEvent(new Event('misairu.ready'))\r\n })\r\n } else if (\r\n typeof audioSource == 'object' &&\r\n (audioSource.tagName == 'AUDIO' || audioSource.tagName == 'VIDEO')\r\n ) {\r\n this._audioSource = attachAudioElementSource(audioSource, this._audioContext)\r\n this._audioSource.connect(this._gainNode)\r\n }\r\n }\r\n\r\n /**\r\n * Mutes the instance audio\r\n *\r\n * @public\r\n */\r\n public mute(): void {\r\n if (!this._muted) {\r\n this._muted = true\r\n this._gainNode.gain.value = 0\r\n }\r\n }\r\n\r\n /**\r\n * Unmutes the instance audio\r\n *\r\n * @public\r\n */\r\n public unmute(): void {\r\n if (this._muted) {\r\n this._muted = false\r\n this._gainNode.gain.value = dbToVolume(this._volume)\r\n }\r\n }\r\n\r\n /**\r\n * Pauses instance playback\r\n *\r\n * @public\r\n */\r\n public pause(): void {\r\n if (!this._paused) {\r\n this._audioContext.suspend()\r\n this._paused = true\r\n }\r\n }\r\n\r\n /**\r\n * Resumes instance playback\r\n *\r\n * @public\r\n */\r\n public unpause(): void {\r\n if (this._paused) {\r\n this._audioContext.resume()\r\n this._paused = false\r\n }\r\n }\r\n\r\n /**\r\n * Set a cache entry for the given track\r\n *\r\n * @param track track to set a cache entry for\r\n * @param entry value of the cache entry\r\n * @internal\r\n */\r\n private setCacheEntry(track: string, entry: string): void {\r\n this._cache[track] = entry\r\n }\r\n\r\n /**\r\n * Get cache entry for the given track\r\n *\r\n * @param track track to get a cache entry for\r\n * @returns a cache entry\r\n * @internal\r\n */\r\n private getCacheEntry(track: string): string {\r\n return this._cache[track]\r\n }\r\n\r\n /**\r\n * Get all tracks from the timing configuration\r\n *\r\n * @returns a list of all track names\r\n * @internal\r\n */\r\n private getAllTracks(): string[] {\r\n return Object.keys(this._timings)\r\n }\r\n\r\n /**\r\n * Returns the current active timing key for a given track\r\n *\r\n * @param track track to get the timing key from\r\n * @param currentTime current playback time\r\n * @returns the current active timing key\r\n * @internal\r\n */\r\n private getActiveTimingKey(track: string, currentTime: number): string {\r\n const timingKeys = Object.keys(this._timings[track])\r\n\r\n const activeTimings = timingKeys.filter((timing) => {\r\n if (currentTime >= parseFloat(timing)) {\r\n return true\r\n }\r\n })\r\n\r\n return activeTimings[activeTimings.length - 1]\r\n }\r\n\r\n /**\r\n * Main method to process special sections in the timing configuration\r\n *\r\n * @internal\r\n */\r\n private processTracks(): void {\r\n this.getAllTracks().forEach((trackName) => {\r\n this._processors.forEach((processor: ITrackProcessor) => {\r\n if (processor.matches(trackName)) {\r\n const [processedTrackName, eventTrack] = processor.process(trackName, this._timings[trackName])\r\n\r\n this._timings[processedTrackName] = eventTrack\r\n\r\n if (processor.deleteOriginTrack) {\r\n delete this._timings[trackName]\r\n }\r\n }\r\n })\r\n })\r\n }\r\n\r\n /**\r\n * Start audio playback and event handling\r\n *\r\n * @public\r\n */\r\n public start(): void {\r\n this._startTime = this._audioContext.currentTime\r\n\r\n ;(this._audioSource as AudioBufferSourceNode).start()\r\n\r\n this.startEventHandling()\r\n }\r\n\r\n /**\r\n * Method to start event handler loop\r\n *\r\n * @internal\r\n */\r\n private startEventHandling(): void {\r\n if (this._eventHandler == null) {\r\n this._eventHandler = window.requestAnimationFrame(() => {\r\n this.handleEvents()\r\n })\r\n }\r\n }\r\n\r\n /**\r\n * Event handler method, is running in a loop using `requestAnimationFrame`\r\n *\r\n * @internal\r\n */\r\n private handleEvents(): void {\r\n const time = this._audioContext.currentTime - this._startTime\r\n\r\n this.getAllTracks().forEach((track) => {\r\n const timingKey = this.getActiveTimingKey(track, time)\r\n\r\n if (timingKey !== null && !(this.getCacheEntry(track) == timingKey)) {\r\n this.executeEvent(track, timingKey, time)\r\n this.setCacheEntry(track, timingKey)\r\n }\r\n })\r\n\r\n this._eventHandler = window.requestAnimationFrame(() => {\r\n this.handleEvents()\r\n })\r\n }\r\n\r\n /**\r\n * Method to execute the event for a given timing key on a given track\r\n *\r\n * @param track the track to execute the event on\r\n * @param timingKey the timing key to execute\r\n * @param time current playback time\r\n * @internal\r\n */\r\n private executeEvent(track: string, timingKey: string, time: number): void {\r\n this._timings[track][timingKey](this, timingKey, track, time)\r\n }\r\n}\r\n","/**\r\n * Method to fetch the external audio file (if the audio source parameter was a string)\r\n * and turning it into a `AudioBufferSourceNode`\r\n *\r\n * @param audioSource the audio source `Misairu` has been constructed with\r\n * @internal\r\n */\r\nexport async function fetchAudioSource(audioSource: string, audioContext: AudioContext): Promise<AudioBufferSourceNode> {\r\n const source = audioContext.createBufferSource()\r\n\r\n const response = await fetch(audioSource)\r\n const arrayBuffer = await response.arrayBuffer()\r\n const buffer = await audioContext.decodeAudioData(arrayBuffer)\r\n\r\n source.buffer = buffer\r\n\r\n return source\r\n}\r\n\r\n/**\r\n * Method to get an `MediaElementAudioSourceNode` from the passed audio source\r\n *\r\n * @param audioSource the audio source `Misairu` has been constructed with\r\n * @internal\r\n */\r\nexport function attachAudioElementSource(audioSource: HTMLMediaElement, audioContext: AudioContext): MediaElementAudioSourceNode {\r\n return audioContext.createMediaElementSource(audioSource)\r\n}"],"names":["REPEAT_NAME_REGEX","RegExp","RepeatTrackProcessor","deleteOriginTrack","matches","name","match","process","track","Error","repeatTrackArgs","split","length","startTime","parseFloat","interval","endTime","time","tempTrack","toString","Math","random","substring","dbToVolume","db","pow","audioSource","timings","_audioContext","_audioSource","_cache","_eventHandler","_gainNode","_muted","_paused","_startTime","_timings","_volume","_processors","console","error","this","AudioContext","createGain","connect","destination","getOptimalAudioSource","processTracks","audioContext","source","createBufferSource","fetch","response","arrayBuffer","decodeAudioData","buffer","fetchAudioSource","then","_this","document","dispatchEvent","Event","tagName","createMediaElementSource","attachAudioElementSource","mute","gain","value","unmute","pause","suspend","unpause","resume","setCacheEntry","entry","getCacheEntry","getAllTracks","Object","keys","getActiveTimingKey","currentTime","activeTimings","filter","timing","forEach","trackName","_this2","processor","start","startEventHandling","window","requestAnimationFrame","_this3","handleEvents","timingKey","_this4","executeEvent","volume"],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { EventFunction, EventTrack, ITrackProcessor } from "../types";
|
|
2
|
+
export declare class RepeatTrackProcessor implements ITrackProcessor {
|
|
3
|
+
deleteOriginTrack: boolean;
|
|
4
|
+
matches(name: string): boolean;
|
|
5
|
+
process(name: string, track: EventFunction): [string, EventTrack];
|
|
6
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Misairu } from './misairu';
|
|
2
|
+
/**
|
|
3
|
+
* Type describing the structure of the event cache
|
|
4
|
+
*/
|
|
5
|
+
export declare type EventCache = {
|
|
6
|
+
[trackName: string]: string;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Type describing the available arguments of event functions
|
|
10
|
+
*/
|
|
11
|
+
export declare type EventFunction = (instance: Misairu, timingKey: string, track: string, time: number) => void;
|
|
12
|
+
/**
|
|
13
|
+
* Type describing the structure of an event track
|
|
14
|
+
*/
|
|
15
|
+
export declare type EventTrack = {
|
|
16
|
+
[timingKey: string]: EventFunction;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Type describing the structure of the timing object
|
|
20
|
+
*/
|
|
21
|
+
export declare type TimingObject = {
|
|
22
|
+
[trackName: string]: EventTrack;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Interface describing required methods/members for track processors
|
|
26
|
+
*/
|
|
27
|
+
export interface ITrackProcessor {
|
|
28
|
+
/**
|
|
29
|
+
* Member describing if the processed track should be deleted
|
|
30
|
+
*/
|
|
31
|
+
deleteOriginTrack: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Method to check if the track name matches to determine if the processor
|
|
34
|
+
* should process it
|
|
35
|
+
*
|
|
36
|
+
* @param name name oƒ a track
|
|
37
|
+
*/
|
|
38
|
+
matches(name: string): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Main processing method
|
|
41
|
+
*
|
|
42
|
+
* @param name name of the track to be processed
|
|
43
|
+
* @param track content of the track to be processed
|
|
44
|
+
*/
|
|
45
|
+
process(name: string, track: EventTrack | EventFunction): [string, EventTrack];
|
|
46
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Method to turn the passed decibel values into volume values for the audio playback
|
|
3
|
+
*
|
|
4
|
+
* @param db decibel value
|
|
5
|
+
* @returns volume value
|
|
6
|
+
*/
|
|
7
|
+
export declare function dbToVolume(db: number): number;
|
|
8
|
+
/**
|
|
9
|
+
* Clamps the volume to a min/max value to prevent accidental oversetting to way too loud measures
|
|
10
|
+
*/
|
|
11
|
+
export declare function clampGain(volume: number): number;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Method to fetch the external audio file (if the audio source parameter was a string)
|
|
3
|
+
* and turning it into a `AudioBufferSourceNode`
|
|
4
|
+
*
|
|
5
|
+
* @param audioSource the audio source `Misairu` has been constructed with
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export declare function fetchAudioSource(audioSource: string, audioContext: AudioContext): Promise<AudioBufferSourceNode>;
|
|
9
|
+
/**
|
|
10
|
+
* Method to get an `MediaElementAudioSourceNode` from the passed audio source
|
|
11
|
+
*
|
|
12
|
+
* @param audioSource the audio source `Misairu` has been constructed with
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
export declare function attachAudioElementSource(audioSource: HTMLMediaElement, audioContext: AudioContext): MediaElementAudioSourceNode;
|
package/package.json
CHANGED
|
@@ -1,26 +1,52 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "misairu",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Fire events for specific timeframes easily",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "misairu",
|
|
3
|
+
"version": "5.0.1",
|
|
4
|
+
"description": "Fire events for specific timeframes easily",
|
|
5
|
+
"source": "src/index.ts",
|
|
6
|
+
"main": "dist/misairu.js",
|
|
7
|
+
"exports": "./dist/misairu.modern.js",
|
|
8
|
+
"module": "dist/misairu.module.js",
|
|
9
|
+
"unpkg": "dist/misairu.umd.js",
|
|
10
|
+
"iife": "dist/misairu.iife.js",
|
|
11
|
+
"types": "dist/index.d.ts",
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "microbundle",
|
|
14
|
+
"postbuild": "microbundle -i src/index.ts -o dist/misairu.iife.js --name Misairu --no-pkg-main -f iife",
|
|
15
|
+
"dev": "microbundle watch",
|
|
16
|
+
"fix": "eslint --fix src/ --ext .ts",
|
|
17
|
+
"lint": "eslint src/ --ext .ts",
|
|
18
|
+
"prepare": "husky install"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://codeberg.org/pixeldesu/misairu.git"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"audio",
|
|
26
|
+
"video",
|
|
27
|
+
"media",
|
|
28
|
+
"events",
|
|
29
|
+
"timing"
|
|
30
|
+
],
|
|
31
|
+
"author": "Andreas Nedbal <andy@pixelde.su>",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://codeberg.org/pixeldesu/misairu/issues"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://codeberg.org/pixeldesu/misairu#readme",
|
|
37
|
+
"lint-staged": {
|
|
38
|
+
"*.{js,ts}": "eslint --fix"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@typescript-eslint/eslint-plugin": "^4.30.0",
|
|
42
|
+
"@typescript-eslint/parser": "^4.30.0",
|
|
43
|
+
"eslint": "^7.32.0",
|
|
44
|
+
"eslint-config-prettier": "^8.3.0",
|
|
45
|
+
"eslint-plugin-prettier": "^4.0.0",
|
|
46
|
+
"husky": "^7.0.2",
|
|
47
|
+
"lint-staged": "^11.1.2",
|
|
48
|
+
"microbundle": "^0.13.3",
|
|
49
|
+
"prettier": "^2.3.2",
|
|
50
|
+
"typescript": "^4.4.2"
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/index.ts
ADDED
package/src/misairu.ts
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { RepeatTrackProcessor } from './processors/repeat'
|
|
2
|
+
import { EventCache, ITrackProcessor, TimingObject } from './types'
|
|
3
|
+
import { dbToVolume, clampGain } from './utilities/audio'
|
|
4
|
+
import { fetchAudioSource, attachAudioElementSource } from './utilities/source'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Main misairu class
|
|
8
|
+
*/
|
|
9
|
+
export class Misairu {
|
|
10
|
+
/**
|
|
11
|
+
* The HTML5 AudioContext used for accurately timing our events
|
|
12
|
+
*/
|
|
13
|
+
private readonly _audioContext: AudioContext
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A source node to play our audio from, either a buffered source from a downloaded media file or a media element source
|
|
17
|
+
*
|
|
18
|
+
* @default null
|
|
19
|
+
*/
|
|
20
|
+
private _audioSource: HTMLMediaElement | AudioBufferSourceNode | MediaElementAudioSourceNode | null = null
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A object containing the last executed time key per track to not execute an event on every tick
|
|
24
|
+
*
|
|
25
|
+
* @default {}
|
|
26
|
+
*/
|
|
27
|
+
private _cache: EventCache = {}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Reference to the `requestAnimationFrame` handler
|
|
31
|
+
*
|
|
32
|
+
* @default null
|
|
33
|
+
*/
|
|
34
|
+
private _eventHandler: number | null = null
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Gain node from our audio context to control audio volume
|
|
38
|
+
*/
|
|
39
|
+
private readonly _gainNode: GainNode
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Boolean value describing if this instance is currently muted
|
|
43
|
+
*/
|
|
44
|
+
private _muted = false
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Boolean value describing if this instance is currently paused
|
|
48
|
+
*/
|
|
49
|
+
private _paused = false
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The time when event handling was started, based on the audio contexts `currentTime` when `start()` was called
|
|
53
|
+
*/
|
|
54
|
+
private _startTime = 0
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Object containing all timing tracks and events to be executed
|
|
58
|
+
*/
|
|
59
|
+
private _timings: TimingObject
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Volume of the current instance
|
|
63
|
+
*/
|
|
64
|
+
private _volume = 0
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* List of (predefined) track processors
|
|
68
|
+
*/
|
|
69
|
+
private _processors: ITrackProcessor[] = [
|
|
70
|
+
new RepeatTrackProcessor()
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
get volume(): number {
|
|
74
|
+
return this._volume
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
set volume(db: number) {
|
|
78
|
+
this._volume = clampGain(db)
|
|
79
|
+
|
|
80
|
+
if (!this._muted) {
|
|
81
|
+
this._gainNode.gain.value = dbToVolume(this._volume)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* misairu constructor
|
|
87
|
+
*
|
|
88
|
+
* @param audioSource a string or HTML element to be used as audio source
|
|
89
|
+
* @param timings a object containing event timing information
|
|
90
|
+
*/
|
|
91
|
+
constructor(audioSource: string | HTMLMediaElement, timings: TimingObject) {
|
|
92
|
+
if (timings === null) console.error('You need to specify a timings object')
|
|
93
|
+
this._timings = timings
|
|
94
|
+
|
|
95
|
+
this._audioContext = new AudioContext()
|
|
96
|
+
|
|
97
|
+
this._gainNode = this._audioContext.createGain()
|
|
98
|
+
this._gainNode.connect(this._audioContext.destination)
|
|
99
|
+
|
|
100
|
+
this.getOptimalAudioSource(audioSource)
|
|
101
|
+
|
|
102
|
+
this.processTracks()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Method to figure out the best course of action to take with the passed audio source
|
|
107
|
+
*
|
|
108
|
+
* @param audioSource the audio source `Misairu` has been constructed with
|
|
109
|
+
* @internal
|
|
110
|
+
*/
|
|
111
|
+
private getOptimalAudioSource(audioSource: string | HTMLMediaElement): void {
|
|
112
|
+
if (typeof audioSource == 'string') {
|
|
113
|
+
fetchAudioSource(audioSource, this._audioContext).then((audioSource) => {
|
|
114
|
+
this._audioSource = audioSource
|
|
115
|
+
this._audioSource.connect(this._gainNode)
|
|
116
|
+
|
|
117
|
+
document.dispatchEvent(new Event('misairu.ready'))
|
|
118
|
+
})
|
|
119
|
+
} else if (
|
|
120
|
+
typeof audioSource == 'object' &&
|
|
121
|
+
(audioSource.tagName == 'AUDIO' || audioSource.tagName == 'VIDEO')
|
|
122
|
+
) {
|
|
123
|
+
this._audioSource = attachAudioElementSource(audioSource, this._audioContext)
|
|
124
|
+
this._audioSource.connect(this._gainNode)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Mutes the instance audio
|
|
130
|
+
*
|
|
131
|
+
* @public
|
|
132
|
+
*/
|
|
133
|
+
public mute(): void {
|
|
134
|
+
if (!this._muted) {
|
|
135
|
+
this._muted = true
|
|
136
|
+
this._gainNode.gain.value = 0
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Unmutes the instance audio
|
|
142
|
+
*
|
|
143
|
+
* @public
|
|
144
|
+
*/
|
|
145
|
+
public unmute(): void {
|
|
146
|
+
if (this._muted) {
|
|
147
|
+
this._muted = false
|
|
148
|
+
this._gainNode.gain.value = dbToVolume(this._volume)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Pauses instance playback
|
|
154
|
+
*
|
|
155
|
+
* @public
|
|
156
|
+
*/
|
|
157
|
+
public pause(): void {
|
|
158
|
+
if (!this._paused) {
|
|
159
|
+
this._audioContext.suspend()
|
|
160
|
+
this._paused = true
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Resumes instance playback
|
|
166
|
+
*
|
|
167
|
+
* @public
|
|
168
|
+
*/
|
|
169
|
+
public unpause(): void {
|
|
170
|
+
if (this._paused) {
|
|
171
|
+
this._audioContext.resume()
|
|
172
|
+
this._paused = false
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Set a cache entry for the given track
|
|
178
|
+
*
|
|
179
|
+
* @param track track to set a cache entry for
|
|
180
|
+
* @param entry value of the cache entry
|
|
181
|
+
* @internal
|
|
182
|
+
*/
|
|
183
|
+
private setCacheEntry(track: string, entry: string): void {
|
|
184
|
+
this._cache[track] = entry
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Get cache entry for the given track
|
|
189
|
+
*
|
|
190
|
+
* @param track track to get a cache entry for
|
|
191
|
+
* @returns a cache entry
|
|
192
|
+
* @internal
|
|
193
|
+
*/
|
|
194
|
+
private getCacheEntry(track: string): string {
|
|
195
|
+
return this._cache[track]
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get all tracks from the timing configuration
|
|
200
|
+
*
|
|
201
|
+
* @returns a list of all track names
|
|
202
|
+
* @internal
|
|
203
|
+
*/
|
|
204
|
+
private getAllTracks(): string[] {
|
|
205
|
+
return Object.keys(this._timings)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Returns the current active timing key for a given track
|
|
210
|
+
*
|
|
211
|
+
* @param track track to get the timing key from
|
|
212
|
+
* @param currentTime current playback time
|
|
213
|
+
* @returns the current active timing key
|
|
214
|
+
* @internal
|
|
215
|
+
*/
|
|
216
|
+
private getActiveTimingKey(track: string, currentTime: number): string {
|
|
217
|
+
const timingKeys = Object.keys(this._timings[track])
|
|
218
|
+
|
|
219
|
+
const activeTimings = timingKeys.filter((timing) => {
|
|
220
|
+
if (currentTime >= parseFloat(timing)) {
|
|
221
|
+
return true
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
return activeTimings[activeTimings.length - 1]
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Main method to process special sections in the timing configuration
|
|
230
|
+
*
|
|
231
|
+
* @internal
|
|
232
|
+
*/
|
|
233
|
+
private processTracks(): void {
|
|
234
|
+
this.getAllTracks().forEach((trackName) => {
|
|
235
|
+
this._processors.forEach((processor: ITrackProcessor) => {
|
|
236
|
+
if (processor.matches(trackName)) {
|
|
237
|
+
const [processedTrackName, eventTrack] = processor.process(trackName, this._timings[trackName])
|
|
238
|
+
|
|
239
|
+
this._timings[processedTrackName] = eventTrack
|
|
240
|
+
|
|
241
|
+
if (processor.deleteOriginTrack) {
|
|
242
|
+
delete this._timings[trackName]
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
})
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Start audio playback and event handling
|
|
251
|
+
*
|
|
252
|
+
* @public
|
|
253
|
+
*/
|
|
254
|
+
public start(): void {
|
|
255
|
+
this._startTime = this._audioContext.currentTime
|
|
256
|
+
|
|
257
|
+
;(this._audioSource as AudioBufferSourceNode).start()
|
|
258
|
+
|
|
259
|
+
this.startEventHandling()
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Method to start event handler loop
|
|
264
|
+
*
|
|
265
|
+
* @internal
|
|
266
|
+
*/
|
|
267
|
+
private startEventHandling(): void {
|
|
268
|
+
if (this._eventHandler == null) {
|
|
269
|
+
this._eventHandler = window.requestAnimationFrame(() => {
|
|
270
|
+
this.handleEvents()
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Event handler method, is running in a loop using `requestAnimationFrame`
|
|
277
|
+
*
|
|
278
|
+
* @internal
|
|
279
|
+
*/
|
|
280
|
+
private handleEvents(): void {
|
|
281
|
+
const time = this._audioContext.currentTime - this._startTime
|
|
282
|
+
|
|
283
|
+
this.getAllTracks().forEach((track) => {
|
|
284
|
+
const timingKey = this.getActiveTimingKey(track, time)
|
|
285
|
+
|
|
286
|
+
if (timingKey !== null && !(this.getCacheEntry(track) == timingKey)) {
|
|
287
|
+
this.executeEvent(track, timingKey, time)
|
|
288
|
+
this.setCacheEntry(track, timingKey)
|
|
289
|
+
}
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
this._eventHandler = window.requestAnimationFrame(() => {
|
|
293
|
+
this.handleEvents()
|
|
294
|
+
})
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Method to execute the event for a given timing key on a given track
|
|
299
|
+
*
|
|
300
|
+
* @param track the track to execute the event on
|
|
301
|
+
* @param timingKey the timing key to execute
|
|
302
|
+
* @param time current playback time
|
|
303
|
+
* @internal
|
|
304
|
+
*/
|
|
305
|
+
private executeEvent(track: string, timingKey: string, time: number): void {
|
|
306
|
+
this._timings[track][timingKey](this, timingKey, track, time)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { EventFunction, EventTrack, ITrackProcessor } from "../types";
|
|
2
|
+
|
|
3
|
+
const REPEAT_NAME_REGEX = new RegExp(/repeat:\d:\d:\d/)
|
|
4
|
+
|
|
5
|
+
export class RepeatTrackProcessor implements ITrackProcessor {
|
|
6
|
+
deleteOriginTrack = true;
|
|
7
|
+
|
|
8
|
+
matches(name: string): boolean {
|
|
9
|
+
return name.match(REPEAT_NAME_REGEX) !== null
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
process(name: string, track: EventFunction): [string, EventTrack] {
|
|
13
|
+
if (typeof track != 'function')
|
|
14
|
+
throw Error(`The value of repeat track "${track}" is not a function`)
|
|
15
|
+
|
|
16
|
+
const repeatTrackArgs = name.split(':')
|
|
17
|
+
|
|
18
|
+
if (repeatTrackArgs.length != 4)
|
|
19
|
+
throw Error(`The repeat track "${name}" does not supply the valid amount of arguments`)
|
|
20
|
+
|
|
21
|
+
const startTime = parseFloat(repeatTrackArgs[1])
|
|
22
|
+
const interval = parseFloat(repeatTrackArgs[2])
|
|
23
|
+
const endTime = parseFloat(repeatTrackArgs[3])
|
|
24
|
+
|
|
25
|
+
let time = startTime
|
|
26
|
+
const tempTrack = {}
|
|
27
|
+
|
|
28
|
+
do {
|
|
29
|
+
tempTrack[time.toString()] = track
|
|
30
|
+
time += interval
|
|
31
|
+
} while (time < endTime)
|
|
32
|
+
|
|
33
|
+
return [`repeat-${Math.random().toString(36).substring(7)}`, tempTrack]
|
|
34
|
+
}
|
|
35
|
+
}
|