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.
@@ -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
+ }
@@ -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.1.0",
4
- "description": "Fire events for specific timeframes easily",
5
- "main": "misairu.js",
6
- "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
8
- },
9
- "repository": {
10
- "type": "git",
11
- "url": "git+https://github.com/pixeldesu/misairu.git"
12
- },
13
- "keywords": [
14
- "audio",
15
- "video",
16
- "media",
17
- "events",
18
- "timing"
19
- ],
20
- "author": "Andreas Nedbal <andy@pixelde.su>",
21
- "license": "MIT",
22
- "bugs": {
23
- "url": "https://github.com/pixeldesu/misairu/issues"
24
- },
25
- "homepage": "https://github.com/pixeldesu/misairu#readme"
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
@@ -0,0 +1,2 @@
1
+ export * from './types'
2
+ export { Misairu as default } from './misairu'
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
+ }