diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 64e78ba..a9ff100 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - feat-loopoffset env: NODE_VERSION: 18 diff --git a/CHANGELOG.md b/CHANGELOG.md index d94e62e..3ae3e07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +## 2.9.0-beta.0 +* @akashic/pdi-types@1.13.0-beta.0 に追従 + * `AudioAsset#loopOffset` に対応 + ## 2.8.3 * `MouseTouchEventHandler` において `button` の値が一部 `PointerEventHandler` と異なってしまう問題を修正 diff --git a/e2e/tests/audio/HTMLAudio.spec.ts b/e2e/tests/audio/HTMLAudio.spec.ts index e0f25be..fd83fea 100644 --- a/e2e/tests/audio/HTMLAudio.spec.ts +++ b/e2e/tests/audio/HTMLAudio.spec.ts @@ -39,7 +39,7 @@ describe("HTMLAudio", () => { const { HTMLAudioPlugin } = require("@akashic/pdi-browser") as typeof index; const plugin = new HTMLAudioPlugin(); const system = new window.__mock__.MockAudioSystem({id: "voice"}); - const asset = plugin.createAsset("id", audioAssetPath, 100, system, false, {}, 10); + const asset = plugin.createAsset("id", audioAssetPath, 100, system, false, {}, 10, undefined); return new Promise((resolve, reject) => { const loader: AssetLoadHandler = { _onAssetLoad: (asset: AudioAsset) => { @@ -65,7 +65,7 @@ describe("HTMLAudio", () => { const { HTMLAudioPlugin } = require("@akashic/pdi-browser") as typeof index; const plugin = new HTMLAudioPlugin(); const system = new window.__mock__.MockAudioSystem({id: "voice"}); - const asset = plugin.createAsset("id", audioAssetPath + "?" + query, 100, system, false, {}, 0); + const asset = plugin.createAsset("id", audioAssetPath + "?" + query, 100, system, false, {}, 0, undefined); return new Promise((resolve, reject) => { const loader: AssetLoadHandler = { _onAssetLoad: (asset: AudioAsset) => { @@ -87,7 +87,7 @@ describe("HTMLAudio", () => { const { HTMLAudioPlugin } = require("@akashic/pdi-browser") as typeof index; const plugin = new HTMLAudioPlugin(); const system = new window.__mock__.MockAudioSystem({id: "voice"}); - const asset = plugin.createAsset("id", "not_found_audio", 100, system, false, {}, 0); + const asset = plugin.createAsset("id", "not_found_audio", 100, system, false, {}, 0, undefined); return new Promise((resolve, reject) => { const loader: AssetLoadHandler = { _onAssetLoad: (_asset: AudioAsset) => { @@ -112,7 +112,7 @@ describe("HTMLAudio", () => { const plugin = new HTMLAudioPlugin(); plugin.supportedFormats = [format]; const system = new window.__mock__.MockAudioSystem({id: "voice"}); - const asset = plugin.createAsset("id", audioAssetPath, 100, system, false, {}, 0); + const asset = plugin.createAsset("id", audioAssetPath, 100, system, false, {}, 0, undefined); return new Promise((resolve) => { const loader: AssetLoadHandler = { _onAssetLoad: (asset: AudioAsset) => { @@ -139,7 +139,7 @@ describe("HTMLAudio", () => { const plugin = new HTMLAudioPlugin(); plugin.supportedFormats = ["aac", "mp4"]; const system = new window.__mock__.MockAudioSystem({id: "voice"}); - const asset = plugin.createAsset("id", audioAsset2Path + "?" + query, 100, system, false, {}, 0); + const asset = plugin.createAsset("id", audioAsset2Path + "?" + query, 100, system, false, {}, 0, undefined); return new Promise((resolve) => { const loader: AssetLoadHandler = { _onAssetLoad: (asset: AudioAsset) => { @@ -168,7 +168,7 @@ describe("HTMLAudio", () => { const plugin = new HTMLAudioPlugin(); plugin.supportedFormats = ["aac", "mp4"]; const system = new window.__mock__.MockAudioSystem({id: "voice"}); - const asset = plugin.createAsset("id", seAssetPath, 100, system, false, {}, 0); + const asset = plugin.createAsset("id", seAssetPath, 100, system, false, {}, 0, undefined); const player = asset.play(); player.changeVolume(0.1); return new Promise<[AudioAsset, AudioPlayer]>((resolve, reject) => { diff --git a/e2e/tests/audio/WebAudio.spec.ts b/e2e/tests/audio/WebAudio.spec.ts index 85edd72..7f9feec 100644 --- a/e2e/tests/audio/WebAudio.spec.ts +++ b/e2e/tests/audio/WebAudio.spec.ts @@ -39,7 +39,7 @@ describe("WebAudio", () => { const { WebAudioPlugin } = require("@akashic/pdi-browser") as typeof index; const plugin = new WebAudioPlugin(); const system = new window.__mock__.MockAudioSystem({id: "voice"}); - const asset = plugin.createAsset("id", audioAssetPath, 100, system, false, {}, 10); + const asset = plugin.createAsset("id", audioAssetPath, 100, system, false, {}, 10, undefined); return new Promise((resolve, reject) => { const loader: AssetLoadHandler = { _onAssetLoad: (asset: AudioAsset) => { @@ -65,7 +65,7 @@ describe("WebAudio", () => { const { WebAudioPlugin } = require("@akashic/pdi-browser") as typeof index; const plugin = new WebAudioPlugin(); const system = new window.__mock__.MockAudioSystem({id: "voice"}); - const asset = plugin.createAsset("id", audioAssetPath + "?" + query, 100, system, false, {}, 0); + const asset = plugin.createAsset("id", audioAssetPath + "?" + query, 100, system, false, {}, 0, undefined); return new Promise((resolve, reject) => { const loader: AssetLoadHandler = { _onAssetLoad: (asset: AudioAsset) => { @@ -87,7 +87,7 @@ describe("WebAudio", () => { const { WebAudioPlugin } = require("@akashic/pdi-browser") as typeof index; const plugin = new WebAudioPlugin(); const system = new window.__mock__.MockAudioSystem({id: "voice"}); - const asset = plugin.createAsset("id", "not_found_audio", 100, system, false, {}, 0); + const asset = plugin.createAsset("id", "not_found_audio", 100, system, false, {}, 0, undefined); return new Promise((resolve, reject) => { const loader: AssetLoadHandler = { _onAssetLoad: (_asset: AudioAsset) => { @@ -113,7 +113,7 @@ describe("WebAudio", () => { const plugin = new WebAudioPlugin(); plugin.supportedFormats = [format]; const system = new window.__mock__.MockAudioSystem({id: "voice"}); - const asset = plugin.createAsset("id", audioAssetPath, 100, system, false, {}, 0); + const asset = plugin.createAsset("id", audioAssetPath, 100, system, false, {}, 0, undefined); return new Promise((resolve) => { const loader: AssetLoadHandler = { _onAssetLoad: (asset: AudioAsset) => { @@ -140,7 +140,7 @@ describe("WebAudio", () => { const plugin = new WebAudioPlugin(); plugin.supportedFormats = ["aac"]; const system = new window.__mock__.MockAudioSystem({id: "voice"}); - const asset = plugin.createAsset("id", audioAsset2Path + "?" + query, 100, system, false, {}, 0); + const asset = plugin.createAsset("id", audioAsset2Path + "?" + query, 100, system, false, {}, 0, undefined); return new Promise((resolve) => { const loader: AssetLoadHandler = { _onAssetLoad: (asset: AudioAsset) => { @@ -168,7 +168,7 @@ describe("WebAudio", () => { const { WebAudioPlugin } = require("@akashic/pdi-browser") as typeof index; const plugin = new WebAudioPlugin(); const system = new window.__mock__.MockAudioSystem({id: "voice"}); - const asset = plugin.createAsset("id", seAssetPath, 100, system, false, {}, 0); + const asset = plugin.createAsset("id", seAssetPath, 100, system, false, {}, 0, undefined); const player = asset.play(); player.changeVolume(0.1); return new Promise<[AudioAsset, AudioPlayer]>((resolve, reject) => { diff --git a/package-lock.json b/package-lock.json index fb8af64..da32d8c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@akashic/pdi-browser", - "version": "2.8.3", + "version": "2.9.0-beta.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@akashic/pdi-browser", - "version": "2.8.3", + "version": "2.9.0-beta.0", "license": "MIT", "dependencies": { "@akashic/trigger": "^2.0.1" @@ -14,7 +14,7 @@ "devDependencies": { "@akashic/amflow": "^3.3.0", "@akashic/eslint-config": "^1.1.1", - "@akashic/pdi-types": "^1.12.0", + "@akashic/pdi-types": "^1.13.0-beta.0", "@akashic/playlog": "^3.3.0", "@types/jest": "^29.2.0", "@types/node": "^18.0.0", @@ -91,9 +91,9 @@ } }, "node_modules/@akashic/pdi-types": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/@akashic/pdi-types/-/pdi-types-1.12.0.tgz", - "integrity": "sha512-kgjUvhdDBFX0oDRy2imdST49T13cSBnRKy09sryWOxMicmq6JvVI3hMuqyyQoc18csr46N8DonkHFSE49skCXA==", + "version": "1.13.0-beta.0", + "resolved": "https://registry.npmjs.org/@akashic/pdi-types/-/pdi-types-1.13.0-beta.0.tgz", + "integrity": "sha512-0sp5sBye5/EcYlAhWfGxV2qkFHPheluI1rYionzS9JojNi24j+aBBrr7oJoynyV5Lf0rSIPcCIlATv4qC/CeVw==", "dev": true, "dependencies": { "@akashic/amflow": "~3.3.0", diff --git a/package.json b/package.json index 0632ac6..afe5595 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@akashic/pdi-browser", - "version": "2.8.3", + "version": "2.9.0-beta.0", "description": "An akashic-pdi implementation for Web browsers", "main": "index.js", "typings": "lib/full/index.d.ts", @@ -43,7 +43,7 @@ "devDependencies": { "@akashic/amflow": "^3.3.0", "@akashic/eslint-config": "^1.1.1", - "@akashic/pdi-types": "^1.12.0", + "@akashic/pdi-types": "^1.13.0-beta.0", "@akashic/playlog": "^3.3.0", "@types/jest": "^29.2.0", "@types/node": "^18.0.0", @@ -74,6 +74,7 @@ "@akashic/trigger": "^2.0.1" }, "publishConfig": { - "@akashic:registry": "https://registry.npmjs.org/" + "@akashic:registry": "https://registry.npmjs.org/", + "tag": "next" } } diff --git a/src/ResourceFactory.ts b/src/ResourceFactory.ts index a5e0eec..47f9e4d 100644 --- a/src/ResourceFactory.ts +++ b/src/ResourceFactory.ts @@ -41,13 +41,14 @@ export class ResourceFactory implements pdi.ResourceFactory { system: pdi.AudioSystem, loop: boolean, hint: pdi.AudioAssetHint, - offset: number + offset: number, + loopOffset: number ): AudioAsset { const activePlugin = this._audioPluginManager.getActivePlugin(); if (!activePlugin) { throw new Error("ResourceFactory#createAudioAsset(): could not initialize ActivePlugin"); } - const audioAsset = activePlugin.createAsset(id, assetPath, duration, system, loop, hint, offset); + const audioAsset = activePlugin.createAsset(id, assetPath, duration, system, loop, hint, offset, loopOffset); this._audioManager.registerAudioAsset(audioAsset); audioAsset.onDestroyed.addOnce(this._onAudioAssetDestroyed, this); return audioAsset; diff --git a/src/asset/AudioAsset.ts b/src/asset/AudioAsset.ts index ad61cde..f140b6b 100644 --- a/src/asset/AudioAsset.ts +++ b/src/asset/AudioAsset.ts @@ -8,6 +8,7 @@ export abstract class AudioAsset extends Asset implements pdi.AudioAsset { loop: boolean; hint: pdi.AudioAssetHint; offset: number; + loopOffset: number | undefined; _system: pdi.AudioSystem; _lastPlayedPlayer: pdi.AudioPlayer | undefined; @@ -18,7 +19,8 @@ export abstract class AudioAsset extends Asset implements pdi.AudioAsset { system: pdi.AudioSystem, loop: boolean, hint: pdi.AudioAssetHint, - offset: number + offset: number, + loopOffset: number | undefined, ) { super(id, path); this.duration = duration; @@ -26,6 +28,7 @@ export abstract class AudioAsset extends Asset implements pdi.AudioAsset { this.hint = hint; this._system = system; this.offset = offset; + this.loopOffset = loopOffset; this.path = this._modifyPath(this.path); } diff --git a/src/plugin/AudioPlugin.ts b/src/plugin/AudioPlugin.ts index 7339186..850645b 100644 --- a/src/plugin/AudioPlugin.ts +++ b/src/plugin/AudioPlugin.ts @@ -19,7 +19,8 @@ export interface AudioPlugin { system: pdi.AudioSystem, loop: boolean, hint: pdi.AudioAssetHint, - offset: number + offset: number, + loopOffset: number, ) => AudioAsset; createPlayer: (system: pdi.AudioSystem, manager: AudioManager) => AudioPlayer; diff --git a/src/plugin/HTMLAudioPlugin/AudioElementPlayer.ts b/src/plugin/HTMLAudioPlugin/AudioElementPlayer.ts new file mode 100644 index 0000000..2b337e1 --- /dev/null +++ b/src/plugin/HTMLAudioPlugin/AudioElementPlayer.ts @@ -0,0 +1,145 @@ +/* eslint-disable @typescript-eslint/typedef */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ + +import { Trigger } from "@akashic/trigger"; +import { setupChromeMEIWorkaround } from "./HTMLAudioAutoplayHelper"; + +export interface AudioElementPlayerParameterObject { + id: string; + element: HTMLAudioElement | null; + duration: number; + offset: number; + loop: boolean; + loopOffset: number; +} + +export class AudioElementPlayer { + onStop: Trigger; + + id: string; + element: HTMLAudioElement | null; + offsetStart: number; + offsetEnd: number; + loopOffset: number; + duration: number; + loop: boolean; + + _dummyTimerId: number | null; + _reachEndTimerId: number | null; + _previousCurrentTime: number; + + constructor({ id, element, duration, offset, loopOffset, loop }: AudioElementPlayerParameterObject) { + this.id = id; + this.duration = duration; + this.offsetStart = offset; + this.offsetEnd = offset + duration; + this.loop = loop; + this.loopOffset = loopOffset; + this.onStop = new Trigger(); + this._dummyTimerId = null; + this._reachEndTimerId = null; + + if (element) { + setupChromeMEIWorkaround(element); + element.addEventListener("timeupdate", this._onTimeupdate, false); + element.addEventListener("ended", this._onEnded, false); + element.currentTime = this.offsetStart / 1000; + } else { + if (!loop && duration != null) { + this._setDummyTimer(this.duration); + } + } + this.element = element; + this._previousCurrentTime = element?.currentTime ?? 0; + } + + rewind() { + this.pause(); + this.setCurrentTime(this.loopOffset || this.offsetStart); + this.play(); + } + + play() { + this.element?.play().catch((_err) => { + // user interact の前に play() を呼ぶとエラーになる。これは HTMLAudioAutoplayHelper で吸収する + }); + } + + pause() { + this.element?.pause(); + this._clearRewindTimer(); + } + + setCurrentTime(offset: number) { + if (!this.element) return; + this.element.currentTime = offset / 1000; + this._previousCurrentTime = this.element.currentTime; + } + + setVolume(volume: number) { + if (!this.element) return; + this.element.volume = volume; + } + + destroy() { + this.onStop.destroy(); + const element = this.element; + if (element) { + element.removeEventListener("timeupdate", this._onTimeupdate, false); + element.removeEventListener("ended", this._onEnded, false); + } + this._clearDummyTimer(); + this._clearRewindTimer(); + } + + _setDummyTimer(duration: number) { + this._clearDummyTimer(); + this._dummyTimerId = window.setTimeout(() => this.pause(), duration); + } + + _clearDummyTimer() { + if (this._dummyTimerId == null) return; + window.clearTimeout(this._dummyTimerId); + this._dummyTimerId = null; + } + + _setRewindTimer(duration: number) { + this._clearRewindTimer(); + this._reachEndTimerId = window.setTimeout(() => this._onReachEnd(), duration); + } + + _clearRewindTimer() { + if (this._reachEndTimerId == null) return; + window.clearTimeout(this._reachEndTimerId); + this._reachEndTimerId = null; + } + + _onReachEnd() { + if (this.loop) { + this.rewind(); + } else { + this.pause(); + this.onStop.fire(); + } + + this._clearRewindTimer(); + } + + _onTimeupdate = () => { + const element = this.element!; // this.element が存在する場合にのみ呼び出される + if (element.paused) return; + + const currentOffset = element.currentTime * 1000; + const deltaSinceLastCall = Math.max(element.currentTime * 1000 - this._previousCurrentTime, 0); + const deltaUntilEnd = Math.max(this.offsetEnd - element.currentTime * 1000, 0); + this._previousCurrentTime = element.currentTime * 1000; + + if (this.offsetEnd < currentOffset + deltaSinceLastCall || this.offsetEnd <= currentOffset) { + this._setRewindTimer(deltaUntilEnd); + } + }; + + _onEnded = () => { + this._onReachEnd(); + }; +} diff --git a/src/plugin/HTMLAudioPlugin/HTMLAudioPlayer.ts b/src/plugin/HTMLAudioPlugin/HTMLAudioPlayer.ts index 6c42a80..03b6d49 100644 --- a/src/plugin/HTMLAudioPlugin/HTMLAudioPlayer.ts +++ b/src/plugin/HTMLAudioPlugin/HTMLAudioPlayer.ts @@ -1,138 +1,64 @@ import type * as pdi from "@akashic/pdi-types"; import type { AudioManager } from "../../AudioManager"; import { AudioPlayer } from "../AudioPlayer"; +import { AudioElementPlayer } from "./AudioElementPlayer"; import type { HTMLAudioAsset } from "./HTMLAudioAsset"; -import { setupChromeMEIWorkaround } from "./HTMLAudioAutoplayHelper"; export class HTMLAudioPlayer extends AudioPlayer { - private _endedEventHandler: () => void; - private _audioInstance: HTMLAudioElement | null = null; private _manager: AudioManager; - private _isWaitingPlayEvent: boolean = false; - private _isStopRequested: boolean = false; - private _onPlayEventHandler: () => void; - private _dummyDurationWaitTimer: any; + private _player: AudioElementPlayer | null; constructor(system: pdi.AudioSystem, manager: AudioManager) { super(system); this._manager = manager; - this._endedEventHandler = () => { - this._onAudioEnded(); - }; - this._onPlayEventHandler = () => { - this._onPlayEvent(); - }; - this._dummyDurationWaitTimer = null; + this._player = null; } play(asset: HTMLAudioAsset): void { - if (this.currentAudio) { - this.stop(); + if (this._player) { + if (asset.id === this._player.id) { + // 同一 ID のアセットは使い回す + super.stop(); + this._player.rewind(); + super.play(asset); + return; + } + this._player.destroy(); } - const audio = asset.cloneElement(); - if (audio) { - if (asset.offset === undefined) { - // offsetが指定されていない場合、durationを無視して全体再生する - audio.loop = asset.loop; - } else { - const offsetSec = (asset.offset ?? 0) / 1000; - const durationEndSec = asset.duration / 1000 + offsetSec; - audio.currentTime = offsetSec; - audio.ontimeupdate = () => { - if (durationEndSec <= audio.currentTime) { - if (asset.loop) { - audio.currentTime = offsetSec; - } else { - audio.pause(); - } - } - }; - audio.onended = () => { - if (asset.loop) { - audio.currentTime = offsetSec; - audio.play(); - } - }; - } + const player = new AudioElementPlayer({ + id: asset.id, + element: asset.cloneElement(), + duration: asset.duration ?? +Infinity, + offset: asset.offset ?? 0, + loop: !!asset.loop, + loopOffset: asset.loopOffset ?? 0, + }); + player.setVolume(this._calculateVolume()); + player.onStop.add(this.stop, this); + player.play(); + this._player = player; - setupChromeMEIWorkaround(audio); - audio.volume = this._calculateVolume(); - audio.play().catch((_err) => { /* user interactの前にplay()を呼ぶとエラーになる。これはHTMLAudioAutoplayHelperで吸収する */}); - audio.addEventListener("ended", this._endedEventHandler, false); - audio.addEventListener("play", this._onPlayEventHandler, false); - this._isWaitingPlayEvent = true; - this._audioInstance = audio; - } else { - // 再生できるオーディオがない場合。duration後に停止処理だけ行う(処理のみ進め音は鳴らさない) - this._dummyDurationWaitTimer = setTimeout(this._endedEventHandler, asset.duration); - } super.play(asset); } stop(): void { - if (!this.currentAudio) { - super.stop(); - return; - } - this._clearEndedEventHandler(); - - if (this._audioInstance) { - if (!this._isWaitingPlayEvent) { - // _audioInstance が再び play されることは無いので、 removeEventListener("play") する必要は無い - this._audioInstance.pause(); - this._audioInstance = null; - } else { - this._isStopRequested = true; - } - } + this._player?.pause(); super.stop(); } changeVolume(volume: number): void { super.changeVolume(volume); - if (this._audioInstance) { - this._audioInstance.volume = this._calculateVolume(); - } + this._player?.setVolume(this._calculateVolume()); } _changeMuted(muted: boolean): void { super._changeMuted(muted); - if (this._audioInstance) { - this._audioInstance.volume = this._calculateVolume(); - } + this._player?.setVolume(this._calculateVolume()); } notifyMasterVolumeChanged(): void { - if (this._audioInstance) { - this._audioInstance.volume = this._calculateVolume(); - } - } - - private _onAudioEnded(): void { - this._clearEndedEventHandler(); - super.stop(); - } - - private _clearEndedEventHandler(): void { - if (this._audioInstance) - this._audioInstance.removeEventListener("ended", this._endedEventHandler, false); - if (this._dummyDurationWaitTimer != null) { - clearTimeout(this._dummyDurationWaitTimer); - this._dummyDurationWaitTimer = null; - } - } - - // audio.play() は非同期なので、 play が開始される前に stop を呼ばれた場合はこのハンドラ到達時に停止する - private _onPlayEvent(): void { - if (!this._isWaitingPlayEvent) return; - this._isWaitingPlayEvent = false; - if (this._isStopRequested) { - this._isStopRequested = false; - // _audioInstance が再び play されることは無いので、 removeEventListener("play") する必要は無い - this._audioInstance?.pause(); - this._audioInstance = null; - } + this._player?.setVolume(this._calculateVolume()); } private _calculateVolume(): number { diff --git a/src/plugin/HTMLAudioPlugin/HTMLAudioPlugin.ts b/src/plugin/HTMLAudioPlugin/HTMLAudioPlugin.ts index 6693bdb..5d40d76 100644 --- a/src/plugin/HTMLAudioPlugin/HTMLAudioPlugin.ts +++ b/src/plugin/HTMLAudioPlugin/HTMLAudioPlugin.ts @@ -46,9 +46,10 @@ export class HTMLAudioPlugin implements AudioPlugin { system: pdi.AudioSystem, loop: boolean, hint: pdi.AudioAssetHint, - offset: number + offset: number, + loopOffset: number | undefined ): AudioAsset { - return new HTMLAudioAsset(id, path, duration, system, loop, hint, offset); + return new HTMLAudioAsset(id, path, duration, system, loop, hint, offset, loopOffset); } createPlayer(system: pdi.AudioSystem, manager: AudioManager): AudioPlayer { diff --git a/src/plugin/HTMLAudioPlugin/__tests__/HTMLAudioAsset.spec.ts b/src/plugin/HTMLAudioPlugin/__tests__/HTMLAudioAsset.spec.ts index 560ed9c..5e65f9c 100644 --- a/src/plugin/HTMLAudioPlugin/__tests__/HTMLAudioAsset.spec.ts +++ b/src/plugin/HTMLAudioPlugin/__tests__/HTMLAudioAsset.spec.ts @@ -8,10 +8,11 @@ describe("HTMLAudioAsset", () => { it("can instantiate", () => { const system = new MockAudioSystem({ id: "audio-system" }); - const asset = new HTMLAudioAsset("audio-asset", "/path/to/audio", 100, system, false, { streaming: false }, 10); + const asset = new HTMLAudioAsset("audio-asset", "/path/to/audio", 100, system, false, { streaming: false }, 10, undefined); expect(asset.id).toBe("audio-asset"); expect(asset.path).toBe("/path/to/audio.ogg"); expect(asset.offset).toBe(10); + expect(asset.loopOffset).toBeUndefined(); expect(asset.duration).toBe(100); expect(asset.hint).toEqual({ streaming: false }); expect(asset.loop).toBe(false); @@ -27,7 +28,8 @@ describe("HTMLAudioAsset", () => { system, false, { streaming: false, extensions: [".m4a", ".aac"] }, - 10 + 10, + undefined ); expect(asset.id).toBe("audio-asset"); expect(asset.path).toBe("/path/to/audio.m4a"); @@ -42,7 +44,8 @@ describe("HTMLAudioAsset", () => { system, false, { streaming: false, extensions: [".mp4", ".aac"] }, - 10 + 10, + undefined ); expect(asset.id).toBe("audio-asset"); expect(asset.path).toBe("/path/to/audio.aac"); diff --git a/src/plugin/HTMLAudioPlugin/__tests__/HTMLAudioPlayer.spec.ts b/src/plugin/HTMLAudioPlugin/__tests__/HTMLAudioPlayer.spec.ts index 2509831..1ae4039 100644 --- a/src/plugin/HTMLAudioPlugin/__tests__/HTMLAudioPlayer.spec.ts +++ b/src/plugin/HTMLAudioPlugin/__tests__/HTMLAudioPlayer.spec.ts @@ -22,7 +22,7 @@ describe("HTMLAudioPlayer", () => { const system = new MockAudioSystem({ id: "audio-system" }); const manager = new AudioManager(); const player = new HTMLAudioPlayer(system, manager); - const asset = new HTMLAudioAsset("audio-asset", "/path/to/audio", 100, system, false, { streaming: false }, 0); + const asset = new HTMLAudioAsset("audio-asset", "/path/to/audio", 100, system, false, { streaming: false }, 0, undefined); expect(player.currentAudio).toBeUndefined(); diff --git a/src/plugin/HTMLAudioPlugin/__tests__/HTMLAudioPlugin.spec.ts b/src/plugin/HTMLAudioPlugin/__tests__/HTMLAudioPlugin.spec.ts index 3ff65fb..6d335cf 100644 --- a/src/plugin/HTMLAudioPlugin/__tests__/HTMLAudioPlugin.spec.ts +++ b/src/plugin/HTMLAudioPlugin/__tests__/HTMLAudioPlugin.spec.ts @@ -13,11 +13,12 @@ describe("HTMLAudioPlugin", () => { const plugin = new HTMLAudioPlugin(); plugin.supportedFormats = ["ogg", "aac"]; const system = new MockAudioSystem({ id: "audio-system" }); - const asset = plugin.createAsset("audio-asset", "/path/to/audio", 100, system, false, { streaming: false }, 10); + const asset = plugin.createAsset("audio-asset", "/path/to/audio", 100, system, false, { streaming: false }, 10, 20); expect(asset.id).toBe("audio-asset"); expect(asset.path).toBe("/path/to/audio.ogg"); expect(asset.offset).toBe(10); + expect(asset.loopOffset).toBe(20); expect(asset.duration).toBe(100); expect(asset.hint).toEqual({ streaming: false }); expect(asset.loop).toBe(false); diff --git a/src/plugin/ProxyAudioPlugin/ProxyAudioAsset.ts b/src/plugin/ProxyAudioPlugin/ProxyAudioAsset.ts index 2479883..617383c 100644 --- a/src/plugin/ProxyAudioPlugin/ProxyAudioAsset.ts +++ b/src/plugin/ProxyAudioPlugin/ProxyAudioAsset.ts @@ -14,9 +14,10 @@ export class ProxyAudioAsset extends AudioAsset { system: pdi.AudioSystem, loop: boolean, hint: pdi.AudioAssetHint, - offset: number + offset: number, + loopOffset: number | undefined, ) { - super(id, assetPath, duration, system, loop, hint, offset); + super(id, assetPath, duration, system, loop, hint, offset, loopOffset); this._handlerSet = handlerSet; } @@ -32,7 +33,8 @@ export class ProxyAudioAsset extends AudioAsset { duration: this.duration, loop: this.loop, hint: this.hint, - offset: this.offset + offset: this.offset, + loopOffset: this.loopOffset }, (err?: any) => { if (err) { loader._onAssetError(this, ExceptionFactory.createAssetLoadError(err)); diff --git a/src/plugin/ProxyAudioPlugin/ProxyAudioHandlerSet.ts b/src/plugin/ProxyAudioPlugin/ProxyAudioHandlerSet.ts index 5edf14a..8039c4a 100644 --- a/src/plugin/ProxyAudioPlugin/ProxyAudioHandlerSet.ts +++ b/src/plugin/ProxyAudioPlugin/ProxyAudioHandlerSet.ts @@ -9,6 +9,7 @@ export interface LoadAudioAssetParameterObject { loop: boolean; hint: AudioAssetHintParameterObject; offset: number; + loopOffset: number | undefined; } export interface CreateAudioPlayerParameterObject { diff --git a/src/plugin/ProxyAudioPlugin/ProxyAudioPlugin.ts b/src/plugin/ProxyAudioPlugin/ProxyAudioPlugin.ts index bd1a9b8..1eb07a6 100644 --- a/src/plugin/ProxyAudioPlugin/ProxyAudioPlugin.ts +++ b/src/plugin/ProxyAudioPlugin/ProxyAudioPlugin.ts @@ -27,9 +27,10 @@ export class ProxyAudioPlugin implements AudioPlugin { system: pdi.AudioSystem, loop: boolean, hint: pdi.AudioAssetHint, - offset: number + offset: number, + loopOffset: number | undefined ): AudioAsset { - return new ProxyAudioAsset(this._handlerSet, id, assetPath, duration, system, loop, hint, offset); + return new ProxyAudioAsset(this._handlerSet, id, assetPath, duration, system, loop, hint, offset, loopOffset); } createPlayer(system: pdi.AudioSystem, manager: AudioManager): AudioPlayer { diff --git a/src/plugin/ProxyAudioPlugin/__tests__/ProxyAudioAsset.spec.ts b/src/plugin/ProxyAudioPlugin/__tests__/ProxyAudioAsset.spec.ts index 5f9b9f3..58243a1 100644 --- a/src/plugin/ProxyAudioPlugin/__tests__/ProxyAudioAsset.spec.ts +++ b/src/plugin/ProxyAudioPlugin/__tests__/ProxyAudioAsset.spec.ts @@ -6,11 +6,22 @@ describe("ProxyAudioAsset", () => { it("can instantiate", () => { const system = new MockAudioSystem({ id: "audio-system" }); const handlerSet = new MockProxyAudioHandlerSet(); - const asset = new ProxyAudioAsset(handlerSet, "audio-asset", "/path/to/audio", 100, system, false, { streaming: false }, 10); + const asset = new ProxyAudioAsset( + handlerSet, + "audio-asset", + "/path/to/audio", + 100, + system, + false, + { streaming: false }, + 10, + undefined + ); expect(asset.id).toBe("audio-asset"); expect(asset.path).toBe("/path/to/audio"); expect(asset.offset).toBe(10); + expect(asset.loopOffset).toBeUndefined(); expect(asset.duration).toBe(100); expect(asset.hint).toEqual({ streaming: false }); expect(asset.loop).toBe(false); @@ -20,7 +31,7 @@ describe("ProxyAudioAsset", () => { it("can load", done => { const system = new MockAudioSystem({ id: "audio-system" }); const handlerSet = new MockProxyAudioHandlerSet(); - const asset = new ProxyAudioAsset(handlerSet, "audio-asset", "/path/to/audio", 100, system, false, { streaming: false }, 10); + const asset = new ProxyAudioAsset(handlerSet, "audio-asset", "/path/to/audio", 100, system, false, { streaming: false }, 10, 20); asset._load({ _onAssetLoad: (a) => { @@ -37,7 +48,7 @@ describe("ProxyAudioAsset", () => { it("can unload", done => { const system = new MockAudioSystem({ id: "audio-system" }); const handlerSet = new MockProxyAudioHandlerSet(); - const asset = new ProxyAudioAsset(handlerSet, "audio-asset", "/path/to/audio", 100, system, false, { streaming: false }, 10); + const asset = new ProxyAudioAsset(handlerSet, "audio-asset", "/path/to/audio", 100, system, false, { streaming: false }, 10, 20); asset._load({ _onAssetLoad: (a) => { diff --git a/src/plugin/ProxyAudioPlugin/__tests__/ProxyAudioPlayer.spec.ts b/src/plugin/ProxyAudioPlugin/__tests__/ProxyAudioPlayer.spec.ts index 7190df1..f3957e5 100644 --- a/src/plugin/ProxyAudioPlugin/__tests__/ProxyAudioPlayer.spec.ts +++ b/src/plugin/ProxyAudioPlugin/__tests__/ProxyAudioPlayer.spec.ts @@ -25,7 +25,7 @@ describe("ProxyAudioPlayer", () => { const manager = new AudioManager(); const handlerSet = new MockProxyAudioHandlerSet(); const player = new ProxyAudioPlayer(handlerSet, system, manager); - const asset = new ProxyAudioAsset(handlerSet, "audio-asset", "/path/to/audio", 100, system, false, { streaming: false }, 0); + const asset = new ProxyAudioAsset(handlerSet, "audio-asset", "/path/to/audio", 100, system, false, { streaming: false }, 0, 20); expect(player.currentAudio).toBeUndefined(); expect(handlerSet.playAudioPlayer).not.toBeCalled(); @@ -44,7 +44,7 @@ describe("ProxyAudioPlayer", () => { const manager = new AudioManager(); const handlerSet = new MockProxyAudioHandlerSet(); const player = new ProxyAudioPlayer(handlerSet, system, manager); - const asset = new ProxyAudioAsset(handlerSet, "audio-asset", "/path/to/audio", 100, system, false, { streaming: false }, 0); + const asset = new ProxyAudioAsset(handlerSet, "audio-asset", "/path/to/audio", 100, system, false, { streaming: false }, 0, 20); player.play(asset); player.play(asset); @@ -57,7 +57,7 @@ describe("ProxyAudioPlayer", () => { const manager = new AudioManager(); const handlerSet = new MockProxyAudioHandlerSet(); const player = new ProxyAudioPlayer(handlerSet, system, manager); - const asset = new ProxyAudioAsset(handlerSet, "audio-asset", "/path/to/audio", 100, system, false, { streaming: false }, 0); + const asset = new ProxyAudioAsset(handlerSet, "audio-asset", "/path/to/audio", 100, system, false, { streaming: false }, 0, 20); expect(handlerSet.createAudioPlayer).not.toBeCalled(); @@ -76,7 +76,7 @@ describe("ProxyAudioPlayer", () => { const manager = new AudioManager(); const handlerSet = new MockProxyAudioHandlerSet(); const player = new ProxyAudioPlayer(handlerSet, system, manager); - const asset = new ProxyAudioAsset(handlerSet, "audio-asset", "/path/to/audio", 100, system, false, { streaming: false }, 0); + const asset = new ProxyAudioAsset(handlerSet, "audio-asset", "/path/to/audio", 100, system, false, { streaming: false }, 0, 20); expect(handlerSet.destroyAudioPlayer).not.toBeCalled(); @@ -90,7 +90,7 @@ describe("ProxyAudioPlayer", () => { const manager = new AudioManager(); const handlerSet = new MockProxyAudioHandlerSet(); const player = new ProxyAudioPlayer(handlerSet, system, manager); - const asset = new ProxyAudioAsset(handlerSet, "audio-asset", "/path/to/audio", 100, system, false, { streaming: false }, 0); + const asset = new ProxyAudioAsset(handlerSet, "audio-asset", "/path/to/audio", 100, system, false, { streaming: false }, 0, 20); expect(handlerSet.changeAudioVolume).not.toBeCalled(); diff --git a/src/plugin/ProxyAudioPlugin/__tests__/ProxyAudioPlugin.spec.ts b/src/plugin/ProxyAudioPlugin/__tests__/ProxyAudioPlugin.spec.ts index d103136..8759532 100644 --- a/src/plugin/ProxyAudioPlugin/__tests__/ProxyAudioPlugin.spec.ts +++ b/src/plugin/ProxyAudioPlugin/__tests__/ProxyAudioPlugin.spec.ts @@ -13,11 +13,12 @@ describe("ProxyAudioPlugin", () => { const handlerSet = new MockProxyAudioHandlerSet(); const plugin = new ProxyAudioPlugin(handlerSet); const system = new MockAudioSystem({ id: "audio-system" }); - const asset = plugin.createAsset("audio-asset", "/path/to/audio", 100, system, false, { streaming: false }, 10); + const asset = plugin.createAsset("audio-asset", "/path/to/audio", 100, system, false, { streaming: false }, 10, 20); expect(asset.id).toBe("audio-asset"); expect(asset.path).toBe("/path/to/audio"); expect(asset.offset).toBe(10); + expect(asset.loopOffset).toBe(20); expect(asset.duration).toBe(100); expect(asset.hint).toEqual({ streaming: false }); expect(asset.loop).toBe(false); diff --git a/src/plugin/WebAudioPlugin/WebAudioPlayer.ts b/src/plugin/WebAudioPlugin/WebAudioPlayer.ts index d18de98..171bdc5 100644 --- a/src/plugin/WebAudioPlugin/WebAudioPlayer.ts +++ b/src/plugin/WebAudioPlugin/WebAudioPlayer.ts @@ -38,6 +38,7 @@ export class WebAudioPlayer extends AudioPlayer { if (this.currentAudio) { this.stop(); } + if (asset.data) { const bufferNode = helper.createBufferNode(this._audioContext); bufferNode.buffer = asset.data; @@ -47,18 +48,20 @@ export class WebAudioPlayer extends AudioPlayer { // Chromeだとevent listerで指定した場合に動かないことがある // https://github.com/mozilla-appmaker/appmaker/issues/1984 this._sourceNode.onended = this._endedEventHandler; - // loop時にoffsetを指定すると正しく動作しないことがあるため、暫定対応としてloopが真の場合はoffsetを指定しない + + bufferNode.loop = asset.loop; + + const offset = (asset.offset ?? 0) / 1000; + const duration = (asset.duration != null) ? asset.duration / 1000 : undefined; if (asset.loop) { - bufferNode.loop = asset.loop; - this._sourceNode.start(0); + bufferNode.loopStart = asset.loopOffset ? (asset.loopOffset / 1000) : offset; + if (duration != null) + bufferNode.loopEnd = offset + duration; + this._sourceNode.start(0, offset); } else { - const offset = (asset.offset ?? 0) / 1000; - if (asset.duration > 0) { - this._sourceNode.start(0, offset, asset.duration / 1000); - } else { - this._sourceNode.start(0, offset); - } + this._sourceNode.start(0, offset, duration); } + } else { // 再生できるオーディオがない場合。duration後に停止処理だけ行う(処理のみ進め音は鳴らさない) this._dummyDurationWaitTimer = setTimeout(this._endedEventHandler, asset.duration); diff --git a/src/plugin/WebAudioPlugin/WebAudioPlugin.ts b/src/plugin/WebAudioPlugin/WebAudioPlugin.ts index b3e8953..5a54425 100644 --- a/src/plugin/WebAudioPlugin/WebAudioPlugin.ts +++ b/src/plugin/WebAudioPlugin/WebAudioPlugin.ts @@ -46,9 +46,10 @@ export class WebAudioPlugin implements AudioPlugin { system: pdi.AudioSystem, loop: boolean, hint: pdi.AudioAssetHint, - offset: number + offset: number, + loopOffset: number | undefined, ): AudioAsset { - return new WebAudioAsset(id, assetPath, duration, system, loop, hint, offset); + return new WebAudioAsset(id, assetPath, duration, system, loop, hint, offset, loopOffset); } createPlayer(system: pdi.AudioSystem, manager: AudioManager): AudioPlayer { diff --git a/src/plugin/WebAudioPlugin/__tests__/WebAudioAsset.spec.ts b/src/plugin/WebAudioPlugin/__tests__/WebAudioAsset.spec.ts index 0e67141..5586edb 100644 --- a/src/plugin/WebAudioPlugin/__tests__/WebAudioAsset.spec.ts +++ b/src/plugin/WebAudioPlugin/__tests__/WebAudioAsset.spec.ts @@ -10,10 +10,11 @@ describe("HTMLAudioAsset", () => { it("can instantiate", () => { const system = new MockAudioSystem({ id: "audio-system" }); - const asset = new WebAudioAsset("audio-asset", "/path/to/audio", 100, system, false, { streaming: false }, 10); + const asset = new WebAudioAsset("audio-asset", "/path/to/audio", 100, system, false, { streaming: false }, 10, 20); expect(asset.id).toBe("audio-asset"); expect(asset.path).toBe("/path/to/audio.ogg"); expect(asset.offset).toBe(10); + expect(asset.loopOffset).toBe(20); expect(asset.duration).toBe(100); expect(asset.hint).toEqual({ streaming: false }); expect(asset.loop).toBe(false); @@ -29,7 +30,8 @@ describe("HTMLAudioAsset", () => { system, false, { streaming: false, extensions: [".m4a", ".aac"] }, - 10 + 10, + undefined ); expect(asset.id).toBe("audio-asset"); expect(asset.path).toBe("/path/to/audio.m4a"); @@ -44,7 +46,8 @@ describe("HTMLAudioAsset", () => { system, false, { streaming: false, extensions: [".mp4", ".aac"] }, - 10 + 10, + undefined ); expect(asset.id).toBe("audio-asset"); expect(asset.path).toBe("/path/to/audio.aac"); diff --git a/src/plugin/WebAudioPlugin/__tests__/WebAudioPlayer.spec.ts b/src/plugin/WebAudioPlugin/__tests__/WebAudioPlayer.spec.ts index 5d1cc66..8b45da8 100644 --- a/src/plugin/WebAudioPlugin/__tests__/WebAudioPlayer.spec.ts +++ b/src/plugin/WebAudioPlugin/__tests__/WebAudioPlayer.spec.ts @@ -24,7 +24,7 @@ describe("WebAudioPlayer", () => { const system = new MockAudioSystem({ id: "audio-system" }); const manager = new AudioManager(); const player = new WebAudioPlayer(system, manager); - const asset = new WebAudioAsset("audio-asset", "/path/to/audio", 100, system, false, { streaming: false }, 0); + const asset = new WebAudioAsset("audio-asset", "/path/to/audio", 100, system, false, { streaming: false }, 0, undefined); expect(player.currentAudio).toBeUndefined(); diff --git a/src/plugin/WebAudioPlugin/__tests__/WebAudioPlugin.spec.ts b/src/plugin/WebAudioPlugin/__tests__/WebAudioPlugin.spec.ts index b36b6a1..a27dcb6 100644 --- a/src/plugin/WebAudioPlugin/__tests__/WebAudioPlugin.spec.ts +++ b/src/plugin/WebAudioPlugin/__tests__/WebAudioPlugin.spec.ts @@ -15,11 +15,12 @@ describe("WebAudioPlugin", () => { const plugin = new WebAudioPlugin(); plugin.supportedFormats = ["ogg", "aac"]; const system = new MockAudioSystem({ id: "audio-system" }); - const asset = plugin.createAsset("audio-asset", "/path/to/audio", 100, system, false, { streaming: false }, 10); + const asset = plugin.createAsset("audio-asset", "/path/to/audio", 100, system, false, { streaming: false }, 10, 20); expect(asset.id).toBe("audio-asset"); expect(asset.path).toBe("/path/to/audio.ogg"); expect(asset.offset).toBe(10); + expect(asset.loopOffset).toBe(20); expect(asset.duration).toBe(100); expect(asset.hint).toEqual({ streaming: false }); expect(asset.loop).toBe(false);