@magmacrunch/adenosine-audio 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/NOTICE +1 -1
- package/README.md +1 -1
- package/dist/index.global.js +21 -5
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +21 -5
- package/dist/index.js.map +1 -1
- package/dist/music.d.ts +2 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,32 @@ any other package.
|
|
|
11
11
|
|
|
12
12
|
_Nothing yet._
|
|
13
13
|
|
|
14
|
+
## 0.3.1 — 2026-09-05
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- `pauseMusic()` no longer loses the playhead. It stopped the source without
|
|
19
|
+
recording how far it had got, and `playMusic()` always called `start(0)`, so
|
|
20
|
+
every resume replayed the track from its opening. The elapsed time is now
|
|
21
|
+
banked from the context clock on pause and passed as the start offset — and
|
|
22
|
+
wrapped into the buffer, since a looping source given an offset past its end
|
|
23
|
+
clamps rather than wrapping, which would pin a resumed loop to its final
|
|
24
|
+
sample.
|
|
25
|
+
|
|
26
|
+
`stopMusic()` and `destroyMusic()` clear that offset. They are ends rather
|
|
27
|
+
than pauses, and the difference was not expressed anywhere before.
|
|
28
|
+
|
|
29
|
+
- `onVisibilityChange()` no longer starts music that was never playing. Its
|
|
30
|
+
resume branch tested only whether a track had been *loaded*, so a page that
|
|
31
|
+
deliberately had not started one — a title screen, or a game whose music
|
|
32
|
+
belongs to a run in progress — began playing on the first tab-return, with
|
|
33
|
+
no `playMusic()` call anywhere in the app. It now resumes only what it
|
|
34
|
+
paused, which also makes its two branches symmetric: the hide branch already
|
|
35
|
+
guarded on `musicStarted`.
|
|
36
|
+
|
|
37
|
+
Reported as [#14](https://github.com/magmacrunch-media/adenosine/issues/14).
|
|
38
|
+
Two of the four games using this module had already routed around it.
|
|
39
|
+
|
|
14
40
|
## 0.3.0 — 2026-08-29
|
|
15
41
|
|
|
16
42
|
### Changed
|
package/NOTICE
CHANGED
package/README.md
CHANGED
|
@@ -57,6 +57,6 @@ ESM only. The `exports` map declares no `require` condition, so this cannot be
|
|
|
57
57
|
|
|
58
58
|
[Apache-2.0](LICENSE) — Copyright 2026 Magma Crunch Media.
|
|
59
59
|
|
|
60
|
-
Part of [adenosine](https://github.com/
|
|
60
|
+
Part of [adenosine](https://github.com/magmacrunch-media/adenosine), a collection
|
|
61
61
|
of lightweight web game engines by [magmacrunch media](https://magmacrunch.com).
|
|
62
62
|
Keep the `NOTICE` file with any copy you distribute.
|
package/dist/index.global.js
CHANGED
|
@@ -71,6 +71,9 @@ var AdAudio = (() => {
|
|
|
71
71
|
var musicMuted = false;
|
|
72
72
|
var musicVolume = 0.3;
|
|
73
73
|
var visibilityHandler = null;
|
|
74
|
+
var musicStartedAt = 0;
|
|
75
|
+
var musicOffset = 0;
|
|
76
|
+
var pausedByVisibility = false;
|
|
74
77
|
async function loadMusic(url, opts) {
|
|
75
78
|
musicVolume = opts?.volume ?? 0.3;
|
|
76
79
|
const res = await fetch(url);
|
|
@@ -88,13 +91,16 @@ var AdAudio = (() => {
|
|
|
88
91
|
musicSource.buffer = musicBuffer;
|
|
89
92
|
musicSource.loop = true;
|
|
90
93
|
musicSource.connect(musicGain);
|
|
91
|
-
|
|
94
|
+
musicOffset = musicBuffer.duration > 0 ? musicOffset % musicBuffer.duration : 0;
|
|
95
|
+
musicSource.start(0, musicOffset);
|
|
96
|
+
musicStartedAt = ctx2.currentTime;
|
|
92
97
|
const target = musicMuted ? 0 : musicVolume;
|
|
93
98
|
musicGain.gain.linearRampToValueAtTime(target, ctx2.currentTime + fadeIn);
|
|
94
99
|
musicStarted = true;
|
|
95
100
|
}
|
|
96
101
|
function pauseMusic() {
|
|
97
102
|
if (musicSource) {
|
|
103
|
+
musicOffset += getCtx().currentTime - musicStartedAt;
|
|
98
104
|
musicSource.onended = null;
|
|
99
105
|
musicSource.stop();
|
|
100
106
|
musicSource = null;
|
|
@@ -103,6 +109,8 @@ var AdAudio = (() => {
|
|
|
103
109
|
}
|
|
104
110
|
function stopMusic() {
|
|
105
111
|
pauseMusic();
|
|
112
|
+
musicOffset = 0;
|
|
113
|
+
pausedByVisibility = false;
|
|
106
114
|
if (musicGain) {
|
|
107
115
|
musicGain.disconnect();
|
|
108
116
|
musicGain = null;
|
|
@@ -145,10 +153,15 @@ var AdAudio = (() => {
|
|
|
145
153
|
document.removeEventListener("visibilitychange", visibilityHandler);
|
|
146
154
|
}
|
|
147
155
|
visibilityHandler = () => {
|
|
148
|
-
if (
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
156
|
+
if (!pause) return;
|
|
157
|
+
if (document.hidden) {
|
|
158
|
+
if (musicStarted) {
|
|
159
|
+
pausedByVisibility = true;
|
|
160
|
+
pauseMusic();
|
|
161
|
+
}
|
|
162
|
+
} else if (pausedByVisibility) {
|
|
163
|
+
pausedByVisibility = false;
|
|
164
|
+
void playMusic(0);
|
|
152
165
|
}
|
|
153
166
|
};
|
|
154
167
|
document.addEventListener("visibilitychange", visibilityHandler);
|
|
@@ -158,6 +171,9 @@ var AdAudio = (() => {
|
|
|
158
171
|
musicBuffer = null;
|
|
159
172
|
musicStarted = false;
|
|
160
173
|
musicMuted = false;
|
|
174
|
+
musicOffset = 0;
|
|
175
|
+
musicStartedAt = 0;
|
|
176
|
+
pausedByVisibility = false;
|
|
161
177
|
if (visibilityHandler) {
|
|
162
178
|
document.removeEventListener("visibilitychange", visibilityHandler);
|
|
163
179
|
visibilityHandler = null;
|
package/dist/index.global.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/audio-context.ts","../src/music.ts","../src/sfx.ts"],"sourcesContent":["import type { AudioManifest } from './types.js';\nimport {\n loadMusic as _loadMusic,\n playMusic as _playMusic,\n pauseMusic as _pauseMusic,\n stopMusic as _stopMusic,\n setMusicVolume as _setMusicVolume,\n setMusicMuted as _setMusicMuted,\n isMusicMuted as _isMusicMuted,\n isMusicPlaying as _isMusicPlaying,\n toggleMusicMute as _toggleMusicMute,\n onVisibilityChange,\n destroyMusic,\n} from './music.js';\nimport {\n loadSfx as _loadSfx,\n playSfx as _playSfx,\n setSfxVolume as _setSfxVolume,\n setSfxGlobalVolume as _setSfxGlobalVolume,\n setSfxMuted as _setSfxMuted,\n isSfxMuted as _isSfxMuted,\n toggleSfxMute as _toggleSfxMute,\n destroySfx,\n} from './sfx.js';\nimport { closeCtx } from './audio-context.js';\n\nlet musicFadeIn = 2.0;\n\nexport async function init(manifest: AudioManifest): Promise<void> {\n if (manifest.music) {\n await _loadMusic(manifest.music.url, {\n volume: manifest.music.volume,\n fadeIn: manifest.music.fadeIn,\n });\n musicFadeIn = manifest.music.fadeIn ?? 2.0;\n }\n\n if (manifest.sfx) {\n const entries = Object.entries(manifest.sfx);\n await Promise.all(\n entries.map(([name, cfg]) => _loadSfx(name, cfg.url, { volume: cfg.volume, pool: cfg.pool }))\n );\n }\n\n if (manifest.autoVisibility !== false) {\n onVisibilityChange(true);\n }\n}\n\nexport async function playMusic(fadeIn?: number): Promise<void> {\n await _playMusic(fadeIn ?? musicFadeIn);\n}\n\nexport function destroy(): void {\n destroyMusic();\n destroySfx();\n closeCtx();\n}\n\nexport {\n _loadMusic as loadMusic,\n _pauseMusic as pauseMusic,\n _stopMusic as stopMusic,\n _setMusicVolume as setMusicVolume,\n _setMusicMuted as setMusicMuted,\n _isMusicMuted as isMusicMuted,\n _isMusicPlaying as isMusicPlaying,\n _toggleMusicMute as toggleMusicMute,\n _loadSfx as loadSfx,\n _playSfx as playSfx,\n _setSfxVolume as setSfxVolume,\n _setSfxGlobalVolume as setSfxGlobalVolume,\n _setSfxMuted as setSfxMuted,\n _isSfxMuted as isSfxMuted,\n _toggleSfxMute as toggleSfxMute,\n onVisibilityChange as handleVisibility,\n};\n\nexport type { AudioManifest, MusicConfig, SfxConfig, VisibilityOptions } from './types.js';\n","let ctx: AudioContext | null = null;\n\nexport function getCtx(): AudioContext {\n if (!ctx) {\n ctx = new (window.AudioContext || (window as any).webkitAudioContext)();\n }\n return ctx;\n}\n\nexport async function resumeCtx(): Promise<void> {\n const c = getCtx();\n if (c.state === 'suspended') {\n await c.resume();\n }\n}\n\nexport function closeCtx(): void {\n if (ctx && ctx.state !== 'closed') {\n ctx.close();\n }\n ctx = null;\n}\n","import { getCtx, resumeCtx } from './audio-context.js';\n\nlet musicBuffer: AudioBuffer | null = null;\nlet musicSource: AudioBufferSourceNode | null = null;\nlet musicGain: GainNode | null = null;\nlet musicStarted = false;\nlet musicMuted = false;\nlet musicVolume = 0.3;\nlet visibilityHandler: (() => void) | null = null;\n\nexport async function loadMusic(url: string, opts?: { volume?: number; fadeIn?: number }): Promise<void> {\n musicVolume = opts?.volume ?? 0.3;\n const res = await fetch(url);\n const arrayBuf = await res.arrayBuffer();\n musicBuffer = await getCtx().decodeAudioData(arrayBuf);\n}\n\nexport async function playMusic(fadeIn = 2.0): Promise<void> {\n if (!musicBuffer || musicStarted) return;\n await resumeCtx();\n\n const ctx = getCtx();\n musicGain = ctx.createGain();\n musicGain.connect(ctx.destination);\n musicGain.gain.setValueAtTime(0, ctx.currentTime);\n\n musicSource = ctx.createBufferSource();\n musicSource.buffer = musicBuffer;\n musicSource.loop = true;\n musicSource.connect(musicGain);\n musicSource.start(0);\n\n const target = musicMuted ? 0 : musicVolume;\n musicGain.gain.linearRampToValueAtTime(target, ctx.currentTime + fadeIn);\n musicStarted = true;\n}\n\nexport function pauseMusic(): void {\n if (musicSource) {\n musicSource.onended = null;\n musicSource.stop();\n musicSource = null;\n }\n musicStarted = false;\n}\n\nexport function stopMusic(): void {\n pauseMusic();\n if (musicGain) {\n musicGain.disconnect();\n musicGain = null;\n }\n}\n\nexport function setMusicVolume(volume: number, rampTime = 0.5): void {\n musicVolume = volume;\n if (musicGain && !musicMuted) {\n const ctx = getCtx();\n musicGain.gain.cancelScheduledValues(ctx.currentTime);\n musicGain.gain.setValueAtTime(musicGain.gain.value, ctx.currentTime);\n musicGain.gain.linearRampToValueAtTime(volume, ctx.currentTime + rampTime);\n }\n}\n\nexport function setMusicMuted(muted: boolean, rampTime = 0.5): void {\n musicMuted = muted;\n if (musicGain) {\n const ctx = getCtx();\n musicGain.gain.cancelScheduledValues(ctx.currentTime);\n if (rampTime <= 0) {\n musicGain.gain.setValueAtTime(muted ? 0 : musicVolume, ctx.currentTime);\n } else {\n musicGain.gain.setValueAtTime(musicGain.gain.value, ctx.currentTime);\n musicGain.gain.linearRampToValueAtTime(muted ? 0 : musicVolume, ctx.currentTime + rampTime);\n }\n }\n}\n\nexport function isMusicMuted(): boolean {\n return musicMuted;\n}\n\nexport function toggleMusicMute(): boolean {\n setMusicMuted(!musicMuted);\n return musicMuted;\n}\n\nexport function isMusicPlaying(): boolean {\n return musicStarted;\n}\n\nexport function onVisibilityChange(pause: boolean): void {\n if (visibilityHandler) {\n document.removeEventListener('visibilitychange', visibilityHandler);\n }\n visibilityHandler = () => {\n if (document.hidden && pause && musicStarted) {\n pauseMusic();\n } else if (!document.hidden && pause && musicBuffer) {\n playMusic(0);\n }\n };\n document.addEventListener('visibilitychange', visibilityHandler);\n}\n\nexport function destroyMusic(): void {\n stopMusic();\n musicBuffer = null;\n musicStarted = false;\n musicMuted = false;\n if (visibilityHandler) {\n document.removeEventListener('visibilitychange', visibilityHandler);\n visibilityHandler = null;\n }\n}\n","import { getCtx, resumeCtx } from './audio-context.js';\n\ninterface SfxEntry {\n buffer: AudioBuffer;\n volume: number;\n pool: AudioBufferSourceNode[];\n poolSize: number;\n}\n\nconst sfxMap = new Map<string, SfxEntry>();\nlet sfxMuted = false;\nlet sfxGlobalVolume = 1.0;\n\nexport async function loadSfx(name: string, url: string, opts?: { volume?: number; pool?: number }): Promise<void> {\n const res = await fetch(url);\n const arrayBuf = await res.arrayBuffer();\n const buffer = await getCtx().decodeAudioData(arrayBuf);\n sfxMap.set(name, {\n buffer,\n volume: opts?.volume ?? 0.5,\n pool: [],\n poolSize: opts?.pool ?? 1,\n });\n}\n\nexport async function playSfx(name: string, opts?: { volume?: number }): Promise<void> {\n if (sfxMuted) return;\n const entry = sfxMap.get(name);\n if (!entry) return;\n await resumeCtx();\n\n const ctx = getCtx();\n const gain = ctx.createGain();\n const vol = (opts?.volume ?? entry.volume) * sfxGlobalVolume;\n gain.gain.setValueAtTime(vol, ctx.currentTime);\n gain.connect(ctx.destination);\n\n const source = ctx.createBufferSource();\n source.buffer = entry.buffer;\n source.connect(gain);\n source.start(0);\n\n source.onended = () => {\n gain.disconnect();\n const idx = entry.pool.indexOf(source);\n if (idx !== -1) entry.pool.splice(idx, 1);\n };\n\n if (entry.pool.length < entry.poolSize) {\n entry.pool.push(source);\n }\n}\n\nexport function setSfxVolume(name: string, volume: number): void {\n const entry = sfxMap.get(name);\n if (entry) entry.volume = volume;\n}\n\nexport function setSfxGlobalVolume(volume: number): void {\n sfxGlobalVolume = volume;\n}\n\nexport function setSfxMuted(muted: boolean): void {\n sfxMuted = muted;\n}\n\nexport function isSfxMuted(): boolean {\n return sfxMuted;\n}\n\nexport function toggleSfxMute(): boolean {\n sfxMuted = !sfxMuted;\n return sfxMuted;\n}\n\nexport function destroySfx(): void {\n sfxMap.clear();\n sfxMuted = false;\n sfxGlobalVolume = 1.0;\n}\n"],"mappings":"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","names":["playMusic","ctx","ctx","playMusic"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/audio-context.ts","../src/music.ts","../src/sfx.ts"],"sourcesContent":["import type { AudioManifest } from './types.js';\nimport {\n loadMusic as _loadMusic,\n playMusic as _playMusic,\n pauseMusic as _pauseMusic,\n stopMusic as _stopMusic,\n setMusicVolume as _setMusicVolume,\n setMusicMuted as _setMusicMuted,\n isMusicMuted as _isMusicMuted,\n isMusicPlaying as _isMusicPlaying,\n toggleMusicMute as _toggleMusicMute,\n onVisibilityChange,\n destroyMusic,\n} from './music.js';\nimport {\n loadSfx as _loadSfx,\n playSfx as _playSfx,\n setSfxVolume as _setSfxVolume,\n setSfxGlobalVolume as _setSfxGlobalVolume,\n setSfxMuted as _setSfxMuted,\n isSfxMuted as _isSfxMuted,\n toggleSfxMute as _toggleSfxMute,\n destroySfx,\n} from './sfx.js';\nimport { closeCtx } from './audio-context.js';\n\nlet musicFadeIn = 2.0;\n\nexport async function init(manifest: AudioManifest): Promise<void> {\n if (manifest.music) {\n await _loadMusic(manifest.music.url, {\n volume: manifest.music.volume,\n fadeIn: manifest.music.fadeIn,\n });\n musicFadeIn = manifest.music.fadeIn ?? 2.0;\n }\n\n if (manifest.sfx) {\n const entries = Object.entries(manifest.sfx);\n await Promise.all(\n entries.map(([name, cfg]) => _loadSfx(name, cfg.url, { volume: cfg.volume, pool: cfg.pool }))\n );\n }\n\n if (manifest.autoVisibility !== false) {\n onVisibilityChange(true);\n }\n}\n\nexport async function playMusic(fadeIn?: number): Promise<void> {\n await _playMusic(fadeIn ?? musicFadeIn);\n}\n\nexport function destroy(): void {\n destroyMusic();\n destroySfx();\n closeCtx();\n}\n\nexport {\n _loadMusic as loadMusic,\n _pauseMusic as pauseMusic,\n _stopMusic as stopMusic,\n _setMusicVolume as setMusicVolume,\n _setMusicMuted as setMusicMuted,\n _isMusicMuted as isMusicMuted,\n _isMusicPlaying as isMusicPlaying,\n _toggleMusicMute as toggleMusicMute,\n _loadSfx as loadSfx,\n _playSfx as playSfx,\n _setSfxVolume as setSfxVolume,\n _setSfxGlobalVolume as setSfxGlobalVolume,\n _setSfxMuted as setSfxMuted,\n _isSfxMuted as isSfxMuted,\n _toggleSfxMute as toggleSfxMute,\n onVisibilityChange as handleVisibility,\n};\n\nexport type { AudioManifest, MusicConfig, SfxConfig, VisibilityOptions } from './types.js';\n","let ctx: AudioContext | null = null;\n\nexport function getCtx(): AudioContext {\n if (!ctx) {\n ctx = new (window.AudioContext || (window as any).webkitAudioContext)();\n }\n return ctx;\n}\n\nexport async function resumeCtx(): Promise<void> {\n const c = getCtx();\n if (c.state === 'suspended') {\n await c.resume();\n }\n}\n\nexport function closeCtx(): void {\n if (ctx && ctx.state !== 'closed') {\n ctx.close();\n }\n ctx = null;\n}\n","import { getCtx, resumeCtx } from './audio-context.js';\n\nlet musicBuffer: AudioBuffer | null = null;\nlet musicSource: AudioBufferSourceNode | null = null;\nlet musicGain: GainNode | null = null;\nlet musicStarted = false;\nlet musicMuted = false;\nlet musicVolume = 0.3;\nlet visibilityHandler: (() => void) | null = null;\n\n/**\n * Where the playhead is, so a pause can be resumed rather than restarted.\n *\n * A BufferSourceNode cannot be restarted once stopped, and it does not report\n * how far it got, so the elapsed time has to be tracked by hand: the context\n * clock is read when a source starts and again when it is stopped.\n *\n * Without this, pauseMusic() threw the position away and playMusic() started\n * the buffer at 0, so a tab-away and back replayed the track from its opening.\n * Easy to miss on a long ambient bed, very audible on a short loop.\n */\nlet musicStartedAt = 0; // ctx.currentTime when the live source began\nlet musicOffset = 0; // seconds into the buffer that source began at\n\n/**\n * Whether the visibility handler is the reason music is not playing.\n *\n * Its resume branch used to fire on any `musicBuffer`, i.e. on nothing more\n * than a track having been loaded — so a page that deliberately had not\n * started its music (a title screen, or a game that only plays during a run)\n * began playing on the first tab-return, with no playMusic() call anywhere in\n * the app. Only what this handler paused is resumed by it.\n */\nlet pausedByVisibility = false;\n\nexport async function loadMusic(url: string, opts?: { volume?: number; fadeIn?: number }): Promise<void> {\n musicVolume = opts?.volume ?? 0.3;\n const res = await fetch(url);\n const arrayBuf = await res.arrayBuffer();\n musicBuffer = await getCtx().decodeAudioData(arrayBuf);\n}\n\nexport async function playMusic(fadeIn = 2.0): Promise<void> {\n if (!musicBuffer || musicStarted) return;\n await resumeCtx();\n\n const ctx = getCtx();\n musicGain = ctx.createGain();\n musicGain.connect(ctx.destination);\n musicGain.gain.setValueAtTime(0, ctx.currentTime);\n\n musicSource = ctx.createBufferSource();\n musicSource.buffer = musicBuffer;\n musicSource.loop = true;\n musicSource.connect(musicGain);\n // Resume where the last pause left off. `loop` means the offset must be\n // wrapped: start() throws nothing for an offset past the buffer, it simply\n // clamps, which would silently pin a resumed loop to the final sample.\n musicOffset = musicBuffer.duration > 0 ? musicOffset % musicBuffer.duration : 0;\n musicSource.start(0, musicOffset);\n musicStartedAt = ctx.currentTime;\n\n const target = musicMuted ? 0 : musicVolume;\n musicGain.gain.linearRampToValueAtTime(target, ctx.currentTime + fadeIn);\n musicStarted = true;\n}\n\n/** Stop playback, remembering the playhead so playMusic() resumes from it. */\nexport function pauseMusic(): void {\n if (musicSource) {\n // Banked before the node goes, since nothing can be read from it after.\n musicOffset += getCtx().currentTime - musicStartedAt;\n musicSource.onended = null;\n musicSource.stop();\n musicSource = null;\n }\n musicStarted = false;\n}\n\n/** End playback. Unlike pauseMusic(), the next play starts from the top. */\nexport function stopMusic(): void {\n pauseMusic();\n musicOffset = 0;\n pausedByVisibility = false;\n if (musicGain) {\n musicGain.disconnect();\n musicGain = null;\n }\n}\n\nexport function setMusicVolume(volume: number, rampTime = 0.5): void {\n musicVolume = volume;\n if (musicGain && !musicMuted) {\n const ctx = getCtx();\n musicGain.gain.cancelScheduledValues(ctx.currentTime);\n musicGain.gain.setValueAtTime(musicGain.gain.value, ctx.currentTime);\n musicGain.gain.linearRampToValueAtTime(volume, ctx.currentTime + rampTime);\n }\n}\n\nexport function setMusicMuted(muted: boolean, rampTime = 0.5): void {\n musicMuted = muted;\n if (musicGain) {\n const ctx = getCtx();\n musicGain.gain.cancelScheduledValues(ctx.currentTime);\n if (rampTime <= 0) {\n musicGain.gain.setValueAtTime(muted ? 0 : musicVolume, ctx.currentTime);\n } else {\n musicGain.gain.setValueAtTime(musicGain.gain.value, ctx.currentTime);\n musicGain.gain.linearRampToValueAtTime(muted ? 0 : musicVolume, ctx.currentTime + rampTime);\n }\n }\n}\n\nexport function isMusicMuted(): boolean {\n return musicMuted;\n}\n\nexport function toggleMusicMute(): boolean {\n setMusicMuted(!musicMuted);\n return musicMuted;\n}\n\nexport function isMusicPlaying(): boolean {\n return musicStarted;\n}\n\nexport function onVisibilityChange(pause: boolean): void {\n if (visibilityHandler) {\n document.removeEventListener('visibilitychange', visibilityHandler);\n }\n visibilityHandler = () => {\n if (!pause) return;\n if (document.hidden) {\n if (musicStarted) {\n pausedByVisibility = true;\n pauseMusic();\n }\n } else if (pausedByVisibility) {\n pausedByVisibility = false;\n void playMusic(0);\n }\n };\n document.addEventListener('visibilitychange', visibilityHandler);\n}\n\nexport function destroyMusic(): void {\n stopMusic();\n musicBuffer = null;\n musicStarted = false;\n musicMuted = false;\n musicOffset = 0;\n musicStartedAt = 0;\n pausedByVisibility = false;\n if (visibilityHandler) {\n document.removeEventListener('visibilitychange', visibilityHandler);\n visibilityHandler = null;\n }\n}\n","import { getCtx, resumeCtx } from './audio-context.js';\n\ninterface SfxEntry {\n buffer: AudioBuffer;\n volume: number;\n pool: AudioBufferSourceNode[];\n poolSize: number;\n}\n\nconst sfxMap = new Map<string, SfxEntry>();\nlet sfxMuted = false;\nlet sfxGlobalVolume = 1.0;\n\nexport async function loadSfx(name: string, url: string, opts?: { volume?: number; pool?: number }): Promise<void> {\n const res = await fetch(url);\n const arrayBuf = await res.arrayBuffer();\n const buffer = await getCtx().decodeAudioData(arrayBuf);\n sfxMap.set(name, {\n buffer,\n volume: opts?.volume ?? 0.5,\n pool: [],\n poolSize: opts?.pool ?? 1,\n });\n}\n\nexport async function playSfx(name: string, opts?: { volume?: number }): Promise<void> {\n if (sfxMuted) return;\n const entry = sfxMap.get(name);\n if (!entry) return;\n await resumeCtx();\n\n const ctx = getCtx();\n const gain = ctx.createGain();\n const vol = (opts?.volume ?? entry.volume) * sfxGlobalVolume;\n gain.gain.setValueAtTime(vol, ctx.currentTime);\n gain.connect(ctx.destination);\n\n const source = ctx.createBufferSource();\n source.buffer = entry.buffer;\n source.connect(gain);\n source.start(0);\n\n source.onended = () => {\n gain.disconnect();\n const idx = entry.pool.indexOf(source);\n if (idx !== -1) entry.pool.splice(idx, 1);\n };\n\n if (entry.pool.length < entry.poolSize) {\n entry.pool.push(source);\n }\n}\n\nexport function setSfxVolume(name: string, volume: number): void {\n const entry = sfxMap.get(name);\n if (entry) entry.volume = volume;\n}\n\nexport function setSfxGlobalVolume(volume: number): void {\n sfxGlobalVolume = volume;\n}\n\nexport function setSfxMuted(muted: boolean): void {\n sfxMuted = muted;\n}\n\nexport function isSfxMuted(): boolean {\n return sfxMuted;\n}\n\nexport function toggleSfxMute(): boolean {\n sfxMuted = !sfxMuted;\n return sfxMuted;\n}\n\nexport function destroySfx(): void {\n sfxMap.clear();\n sfxMuted = false;\n sfxGlobalVolume = 1.0;\n}\n"],"mappings":"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","names":["playMusic","ctx","ctx","playMusic"]}
|
package/dist/index.js
CHANGED
|
@@ -27,6 +27,9 @@ var musicStarted = false;
|
|
|
27
27
|
var musicMuted = false;
|
|
28
28
|
var musicVolume = 0.3;
|
|
29
29
|
var visibilityHandler = null;
|
|
30
|
+
var musicStartedAt = 0;
|
|
31
|
+
var musicOffset = 0;
|
|
32
|
+
var pausedByVisibility = false;
|
|
30
33
|
async function loadMusic(url, opts) {
|
|
31
34
|
musicVolume = opts?.volume ?? 0.3;
|
|
32
35
|
const res = await fetch(url);
|
|
@@ -44,13 +47,16 @@ async function playMusic(fadeIn = 2) {
|
|
|
44
47
|
musicSource.buffer = musicBuffer;
|
|
45
48
|
musicSource.loop = true;
|
|
46
49
|
musicSource.connect(musicGain);
|
|
47
|
-
|
|
50
|
+
musicOffset = musicBuffer.duration > 0 ? musicOffset % musicBuffer.duration : 0;
|
|
51
|
+
musicSource.start(0, musicOffset);
|
|
52
|
+
musicStartedAt = ctx2.currentTime;
|
|
48
53
|
const target = musicMuted ? 0 : musicVolume;
|
|
49
54
|
musicGain.gain.linearRampToValueAtTime(target, ctx2.currentTime + fadeIn);
|
|
50
55
|
musicStarted = true;
|
|
51
56
|
}
|
|
52
57
|
function pauseMusic() {
|
|
53
58
|
if (musicSource) {
|
|
59
|
+
musicOffset += getCtx().currentTime - musicStartedAt;
|
|
54
60
|
musicSource.onended = null;
|
|
55
61
|
musicSource.stop();
|
|
56
62
|
musicSource = null;
|
|
@@ -59,6 +65,8 @@ function pauseMusic() {
|
|
|
59
65
|
}
|
|
60
66
|
function stopMusic() {
|
|
61
67
|
pauseMusic();
|
|
68
|
+
musicOffset = 0;
|
|
69
|
+
pausedByVisibility = false;
|
|
62
70
|
if (musicGain) {
|
|
63
71
|
musicGain.disconnect();
|
|
64
72
|
musicGain = null;
|
|
@@ -101,10 +109,15 @@ function onVisibilityChange(pause) {
|
|
|
101
109
|
document.removeEventListener("visibilitychange", visibilityHandler);
|
|
102
110
|
}
|
|
103
111
|
visibilityHandler = () => {
|
|
104
|
-
if (
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
112
|
+
if (!pause) return;
|
|
113
|
+
if (document.hidden) {
|
|
114
|
+
if (musicStarted) {
|
|
115
|
+
pausedByVisibility = true;
|
|
116
|
+
pauseMusic();
|
|
117
|
+
}
|
|
118
|
+
} else if (pausedByVisibility) {
|
|
119
|
+
pausedByVisibility = false;
|
|
120
|
+
void playMusic(0);
|
|
108
121
|
}
|
|
109
122
|
};
|
|
110
123
|
document.addEventListener("visibilitychange", visibilityHandler);
|
|
@@ -114,6 +127,9 @@ function destroyMusic() {
|
|
|
114
127
|
musicBuffer = null;
|
|
115
128
|
musicStarted = false;
|
|
116
129
|
musicMuted = false;
|
|
130
|
+
musicOffset = 0;
|
|
131
|
+
musicStartedAt = 0;
|
|
132
|
+
pausedByVisibility = false;
|
|
117
133
|
if (visibilityHandler) {
|
|
118
134
|
document.removeEventListener("visibilitychange", visibilityHandler);
|
|
119
135
|
visibilityHandler = null;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/audio-context.ts","../src/music.ts","../src/sfx.ts","../src/index.ts"],"sourcesContent":["let ctx: AudioContext | null = null;\n\nexport function getCtx(): AudioContext {\n if (!ctx) {\n ctx = new (window.AudioContext || (window as any).webkitAudioContext)();\n }\n return ctx;\n}\n\nexport async function resumeCtx(): Promise<void> {\n const c = getCtx();\n if (c.state === 'suspended') {\n await c.resume();\n }\n}\n\nexport function closeCtx(): void {\n if (ctx && ctx.state !== 'closed') {\n ctx.close();\n }\n ctx = null;\n}\n","import { getCtx, resumeCtx } from './audio-context.js';\n\nlet musicBuffer: AudioBuffer | null = null;\nlet musicSource: AudioBufferSourceNode | null = null;\nlet musicGain: GainNode | null = null;\nlet musicStarted = false;\nlet musicMuted = false;\nlet musicVolume = 0.3;\nlet visibilityHandler: (() => void) | null = null;\n\nexport async function loadMusic(url: string, opts?: { volume?: number; fadeIn?: number }): Promise<void> {\n musicVolume = opts?.volume ?? 0.3;\n const res = await fetch(url);\n const arrayBuf = await res.arrayBuffer();\n musicBuffer = await getCtx().decodeAudioData(arrayBuf);\n}\n\nexport async function playMusic(fadeIn = 2.0): Promise<void> {\n if (!musicBuffer || musicStarted) return;\n await resumeCtx();\n\n const ctx = getCtx();\n musicGain = ctx.createGain();\n musicGain.connect(ctx.destination);\n musicGain.gain.setValueAtTime(0, ctx.currentTime);\n\n musicSource = ctx.createBufferSource();\n musicSource.buffer = musicBuffer;\n musicSource.loop = true;\n musicSource.connect(musicGain);\n musicSource.start(0);\n\n const target = musicMuted ? 0 : musicVolume;\n musicGain.gain.linearRampToValueAtTime(target, ctx.currentTime + fadeIn);\n musicStarted = true;\n}\n\nexport function pauseMusic(): void {\n if (musicSource) {\n musicSource.onended = null;\n musicSource.stop();\n musicSource = null;\n }\n musicStarted = false;\n}\n\nexport function stopMusic(): void {\n pauseMusic();\n if (musicGain) {\n musicGain.disconnect();\n musicGain = null;\n }\n}\n\nexport function setMusicVolume(volume: number, rampTime = 0.5): void {\n musicVolume = volume;\n if (musicGain && !musicMuted) {\n const ctx = getCtx();\n musicGain.gain.cancelScheduledValues(ctx.currentTime);\n musicGain.gain.setValueAtTime(musicGain.gain.value, ctx.currentTime);\n musicGain.gain.linearRampToValueAtTime(volume, ctx.currentTime + rampTime);\n }\n}\n\nexport function setMusicMuted(muted: boolean, rampTime = 0.5): void {\n musicMuted = muted;\n if (musicGain) {\n const ctx = getCtx();\n musicGain.gain.cancelScheduledValues(ctx.currentTime);\n if (rampTime <= 0) {\n musicGain.gain.setValueAtTime(muted ? 0 : musicVolume, ctx.currentTime);\n } else {\n musicGain.gain.setValueAtTime(musicGain.gain.value, ctx.currentTime);\n musicGain.gain.linearRampToValueAtTime(muted ? 0 : musicVolume, ctx.currentTime + rampTime);\n }\n }\n}\n\nexport function isMusicMuted(): boolean {\n return musicMuted;\n}\n\nexport function toggleMusicMute(): boolean {\n setMusicMuted(!musicMuted);\n return musicMuted;\n}\n\nexport function isMusicPlaying(): boolean {\n return musicStarted;\n}\n\nexport function onVisibilityChange(pause: boolean): void {\n if (visibilityHandler) {\n document.removeEventListener('visibilitychange', visibilityHandler);\n }\n visibilityHandler = () => {\n if (document.hidden && pause && musicStarted) {\n pauseMusic();\n } else if (!document.hidden && pause && musicBuffer) {\n playMusic(0);\n }\n };\n document.addEventListener('visibilitychange', visibilityHandler);\n}\n\nexport function destroyMusic(): void {\n stopMusic();\n musicBuffer = null;\n musicStarted = false;\n musicMuted = false;\n if (visibilityHandler) {\n document.removeEventListener('visibilitychange', visibilityHandler);\n visibilityHandler = null;\n }\n}\n","import { getCtx, resumeCtx } from './audio-context.js';\n\ninterface SfxEntry {\n buffer: AudioBuffer;\n volume: number;\n pool: AudioBufferSourceNode[];\n poolSize: number;\n}\n\nconst sfxMap = new Map<string, SfxEntry>();\nlet sfxMuted = false;\nlet sfxGlobalVolume = 1.0;\n\nexport async function loadSfx(name: string, url: string, opts?: { volume?: number; pool?: number }): Promise<void> {\n const res = await fetch(url);\n const arrayBuf = await res.arrayBuffer();\n const buffer = await getCtx().decodeAudioData(arrayBuf);\n sfxMap.set(name, {\n buffer,\n volume: opts?.volume ?? 0.5,\n pool: [],\n poolSize: opts?.pool ?? 1,\n });\n}\n\nexport async function playSfx(name: string, opts?: { volume?: number }): Promise<void> {\n if (sfxMuted) return;\n const entry = sfxMap.get(name);\n if (!entry) return;\n await resumeCtx();\n\n const ctx = getCtx();\n const gain = ctx.createGain();\n const vol = (opts?.volume ?? entry.volume) * sfxGlobalVolume;\n gain.gain.setValueAtTime(vol, ctx.currentTime);\n gain.connect(ctx.destination);\n\n const source = ctx.createBufferSource();\n source.buffer = entry.buffer;\n source.connect(gain);\n source.start(0);\n\n source.onended = () => {\n gain.disconnect();\n const idx = entry.pool.indexOf(source);\n if (idx !== -1) entry.pool.splice(idx, 1);\n };\n\n if (entry.pool.length < entry.poolSize) {\n entry.pool.push(source);\n }\n}\n\nexport function setSfxVolume(name: string, volume: number): void {\n const entry = sfxMap.get(name);\n if (entry) entry.volume = volume;\n}\n\nexport function setSfxGlobalVolume(volume: number): void {\n sfxGlobalVolume = volume;\n}\n\nexport function setSfxMuted(muted: boolean): void {\n sfxMuted = muted;\n}\n\nexport function isSfxMuted(): boolean {\n return sfxMuted;\n}\n\nexport function toggleSfxMute(): boolean {\n sfxMuted = !sfxMuted;\n return sfxMuted;\n}\n\nexport function destroySfx(): void {\n sfxMap.clear();\n sfxMuted = false;\n sfxGlobalVolume = 1.0;\n}\n","import type { AudioManifest } from './types.js';\nimport {\n loadMusic as _loadMusic,\n playMusic as _playMusic,\n pauseMusic as _pauseMusic,\n stopMusic as _stopMusic,\n setMusicVolume as _setMusicVolume,\n setMusicMuted as _setMusicMuted,\n isMusicMuted as _isMusicMuted,\n isMusicPlaying as _isMusicPlaying,\n toggleMusicMute as _toggleMusicMute,\n onVisibilityChange,\n destroyMusic,\n} from './music.js';\nimport {\n loadSfx as _loadSfx,\n playSfx as _playSfx,\n setSfxVolume as _setSfxVolume,\n setSfxGlobalVolume as _setSfxGlobalVolume,\n setSfxMuted as _setSfxMuted,\n isSfxMuted as _isSfxMuted,\n toggleSfxMute as _toggleSfxMute,\n destroySfx,\n} from './sfx.js';\nimport { closeCtx } from './audio-context.js';\n\nlet musicFadeIn = 2.0;\n\nexport async function init(manifest: AudioManifest): Promise<void> {\n if (manifest.music) {\n await _loadMusic(manifest.music.url, {\n volume: manifest.music.volume,\n fadeIn: manifest.music.fadeIn,\n });\n musicFadeIn = manifest.music.fadeIn ?? 2.0;\n }\n\n if (manifest.sfx) {\n const entries = Object.entries(manifest.sfx);\n await Promise.all(\n entries.map(([name, cfg]) => _loadSfx(name, cfg.url, { volume: cfg.volume, pool: cfg.pool }))\n );\n }\n\n if (manifest.autoVisibility !== false) {\n onVisibilityChange(true);\n }\n}\n\nexport async function playMusic(fadeIn?: number): Promise<void> {\n await _playMusic(fadeIn ?? musicFadeIn);\n}\n\nexport function destroy(): void {\n destroyMusic();\n destroySfx();\n closeCtx();\n}\n\nexport {\n _loadMusic as loadMusic,\n _pauseMusic as pauseMusic,\n _stopMusic as stopMusic,\n _setMusicVolume as setMusicVolume,\n _setMusicMuted as setMusicMuted,\n _isMusicMuted as isMusicMuted,\n _isMusicPlaying as isMusicPlaying,\n _toggleMusicMute as toggleMusicMute,\n _loadSfx as loadSfx,\n _playSfx as playSfx,\n _setSfxVolume as setSfxVolume,\n _setSfxGlobalVolume as setSfxGlobalVolume,\n _setSfxMuted as setSfxMuted,\n _isSfxMuted as isSfxMuted,\n _toggleSfxMute as toggleSfxMute,\n onVisibilityChange as handleVisibility,\n};\n\nexport type { AudioManifest, MusicConfig, SfxConfig, VisibilityOptions } from './types.js';\n"],"mappings":"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","names":["ctx","ctx","playMusic"]}
|
|
1
|
+
{"version":3,"sources":["../src/audio-context.ts","../src/music.ts","../src/sfx.ts","../src/index.ts"],"sourcesContent":["let ctx: AudioContext | null = null;\n\nexport function getCtx(): AudioContext {\n if (!ctx) {\n ctx = new (window.AudioContext || (window as any).webkitAudioContext)();\n }\n return ctx;\n}\n\nexport async function resumeCtx(): Promise<void> {\n const c = getCtx();\n if (c.state === 'suspended') {\n await c.resume();\n }\n}\n\nexport function closeCtx(): void {\n if (ctx && ctx.state !== 'closed') {\n ctx.close();\n }\n ctx = null;\n}\n","import { getCtx, resumeCtx } from './audio-context.js';\n\nlet musicBuffer: AudioBuffer | null = null;\nlet musicSource: AudioBufferSourceNode | null = null;\nlet musicGain: GainNode | null = null;\nlet musicStarted = false;\nlet musicMuted = false;\nlet musicVolume = 0.3;\nlet visibilityHandler: (() => void) | null = null;\n\n/**\n * Where the playhead is, so a pause can be resumed rather than restarted.\n *\n * A BufferSourceNode cannot be restarted once stopped, and it does not report\n * how far it got, so the elapsed time has to be tracked by hand: the context\n * clock is read when a source starts and again when it is stopped.\n *\n * Without this, pauseMusic() threw the position away and playMusic() started\n * the buffer at 0, so a tab-away and back replayed the track from its opening.\n * Easy to miss on a long ambient bed, very audible on a short loop.\n */\nlet musicStartedAt = 0; // ctx.currentTime when the live source began\nlet musicOffset = 0; // seconds into the buffer that source began at\n\n/**\n * Whether the visibility handler is the reason music is not playing.\n *\n * Its resume branch used to fire on any `musicBuffer`, i.e. on nothing more\n * than a track having been loaded — so a page that deliberately had not\n * started its music (a title screen, or a game that only plays during a run)\n * began playing on the first tab-return, with no playMusic() call anywhere in\n * the app. Only what this handler paused is resumed by it.\n */\nlet pausedByVisibility = false;\n\nexport async function loadMusic(url: string, opts?: { volume?: number; fadeIn?: number }): Promise<void> {\n musicVolume = opts?.volume ?? 0.3;\n const res = await fetch(url);\n const arrayBuf = await res.arrayBuffer();\n musicBuffer = await getCtx().decodeAudioData(arrayBuf);\n}\n\nexport async function playMusic(fadeIn = 2.0): Promise<void> {\n if (!musicBuffer || musicStarted) return;\n await resumeCtx();\n\n const ctx = getCtx();\n musicGain = ctx.createGain();\n musicGain.connect(ctx.destination);\n musicGain.gain.setValueAtTime(0, ctx.currentTime);\n\n musicSource = ctx.createBufferSource();\n musicSource.buffer = musicBuffer;\n musicSource.loop = true;\n musicSource.connect(musicGain);\n // Resume where the last pause left off. `loop` means the offset must be\n // wrapped: start() throws nothing for an offset past the buffer, it simply\n // clamps, which would silently pin a resumed loop to the final sample.\n musicOffset = musicBuffer.duration > 0 ? musicOffset % musicBuffer.duration : 0;\n musicSource.start(0, musicOffset);\n musicStartedAt = ctx.currentTime;\n\n const target = musicMuted ? 0 : musicVolume;\n musicGain.gain.linearRampToValueAtTime(target, ctx.currentTime + fadeIn);\n musicStarted = true;\n}\n\n/** Stop playback, remembering the playhead so playMusic() resumes from it. */\nexport function pauseMusic(): void {\n if (musicSource) {\n // Banked before the node goes, since nothing can be read from it after.\n musicOffset += getCtx().currentTime - musicStartedAt;\n musicSource.onended = null;\n musicSource.stop();\n musicSource = null;\n }\n musicStarted = false;\n}\n\n/** End playback. Unlike pauseMusic(), the next play starts from the top. */\nexport function stopMusic(): void {\n pauseMusic();\n musicOffset = 0;\n pausedByVisibility = false;\n if (musicGain) {\n musicGain.disconnect();\n musicGain = null;\n }\n}\n\nexport function setMusicVolume(volume: number, rampTime = 0.5): void {\n musicVolume = volume;\n if (musicGain && !musicMuted) {\n const ctx = getCtx();\n musicGain.gain.cancelScheduledValues(ctx.currentTime);\n musicGain.gain.setValueAtTime(musicGain.gain.value, ctx.currentTime);\n musicGain.gain.linearRampToValueAtTime(volume, ctx.currentTime + rampTime);\n }\n}\n\nexport function setMusicMuted(muted: boolean, rampTime = 0.5): void {\n musicMuted = muted;\n if (musicGain) {\n const ctx = getCtx();\n musicGain.gain.cancelScheduledValues(ctx.currentTime);\n if (rampTime <= 0) {\n musicGain.gain.setValueAtTime(muted ? 0 : musicVolume, ctx.currentTime);\n } else {\n musicGain.gain.setValueAtTime(musicGain.gain.value, ctx.currentTime);\n musicGain.gain.linearRampToValueAtTime(muted ? 0 : musicVolume, ctx.currentTime + rampTime);\n }\n }\n}\n\nexport function isMusicMuted(): boolean {\n return musicMuted;\n}\n\nexport function toggleMusicMute(): boolean {\n setMusicMuted(!musicMuted);\n return musicMuted;\n}\n\nexport function isMusicPlaying(): boolean {\n return musicStarted;\n}\n\nexport function onVisibilityChange(pause: boolean): void {\n if (visibilityHandler) {\n document.removeEventListener('visibilitychange', visibilityHandler);\n }\n visibilityHandler = () => {\n if (!pause) return;\n if (document.hidden) {\n if (musicStarted) {\n pausedByVisibility = true;\n pauseMusic();\n }\n } else if (pausedByVisibility) {\n pausedByVisibility = false;\n void playMusic(0);\n }\n };\n document.addEventListener('visibilitychange', visibilityHandler);\n}\n\nexport function destroyMusic(): void {\n stopMusic();\n musicBuffer = null;\n musicStarted = false;\n musicMuted = false;\n musicOffset = 0;\n musicStartedAt = 0;\n pausedByVisibility = false;\n if (visibilityHandler) {\n document.removeEventListener('visibilitychange', visibilityHandler);\n visibilityHandler = null;\n }\n}\n","import { getCtx, resumeCtx } from './audio-context.js';\n\ninterface SfxEntry {\n buffer: AudioBuffer;\n volume: number;\n pool: AudioBufferSourceNode[];\n poolSize: number;\n}\n\nconst sfxMap = new Map<string, SfxEntry>();\nlet sfxMuted = false;\nlet sfxGlobalVolume = 1.0;\n\nexport async function loadSfx(name: string, url: string, opts?: { volume?: number; pool?: number }): Promise<void> {\n const res = await fetch(url);\n const arrayBuf = await res.arrayBuffer();\n const buffer = await getCtx().decodeAudioData(arrayBuf);\n sfxMap.set(name, {\n buffer,\n volume: opts?.volume ?? 0.5,\n pool: [],\n poolSize: opts?.pool ?? 1,\n });\n}\n\nexport async function playSfx(name: string, opts?: { volume?: number }): Promise<void> {\n if (sfxMuted) return;\n const entry = sfxMap.get(name);\n if (!entry) return;\n await resumeCtx();\n\n const ctx = getCtx();\n const gain = ctx.createGain();\n const vol = (opts?.volume ?? entry.volume) * sfxGlobalVolume;\n gain.gain.setValueAtTime(vol, ctx.currentTime);\n gain.connect(ctx.destination);\n\n const source = ctx.createBufferSource();\n source.buffer = entry.buffer;\n source.connect(gain);\n source.start(0);\n\n source.onended = () => {\n gain.disconnect();\n const idx = entry.pool.indexOf(source);\n if (idx !== -1) entry.pool.splice(idx, 1);\n };\n\n if (entry.pool.length < entry.poolSize) {\n entry.pool.push(source);\n }\n}\n\nexport function setSfxVolume(name: string, volume: number): void {\n const entry = sfxMap.get(name);\n if (entry) entry.volume = volume;\n}\n\nexport function setSfxGlobalVolume(volume: number): void {\n sfxGlobalVolume = volume;\n}\n\nexport function setSfxMuted(muted: boolean): void {\n sfxMuted = muted;\n}\n\nexport function isSfxMuted(): boolean {\n return sfxMuted;\n}\n\nexport function toggleSfxMute(): boolean {\n sfxMuted = !sfxMuted;\n return sfxMuted;\n}\n\nexport function destroySfx(): void {\n sfxMap.clear();\n sfxMuted = false;\n sfxGlobalVolume = 1.0;\n}\n","import type { AudioManifest } from './types.js';\nimport {\n loadMusic as _loadMusic,\n playMusic as _playMusic,\n pauseMusic as _pauseMusic,\n stopMusic as _stopMusic,\n setMusicVolume as _setMusicVolume,\n setMusicMuted as _setMusicMuted,\n isMusicMuted as _isMusicMuted,\n isMusicPlaying as _isMusicPlaying,\n toggleMusicMute as _toggleMusicMute,\n onVisibilityChange,\n destroyMusic,\n} from './music.js';\nimport {\n loadSfx as _loadSfx,\n playSfx as _playSfx,\n setSfxVolume as _setSfxVolume,\n setSfxGlobalVolume as _setSfxGlobalVolume,\n setSfxMuted as _setSfxMuted,\n isSfxMuted as _isSfxMuted,\n toggleSfxMute as _toggleSfxMute,\n destroySfx,\n} from './sfx.js';\nimport { closeCtx } from './audio-context.js';\n\nlet musicFadeIn = 2.0;\n\nexport async function init(manifest: AudioManifest): Promise<void> {\n if (manifest.music) {\n await _loadMusic(manifest.music.url, {\n volume: manifest.music.volume,\n fadeIn: manifest.music.fadeIn,\n });\n musicFadeIn = manifest.music.fadeIn ?? 2.0;\n }\n\n if (manifest.sfx) {\n const entries = Object.entries(manifest.sfx);\n await Promise.all(\n entries.map(([name, cfg]) => _loadSfx(name, cfg.url, { volume: cfg.volume, pool: cfg.pool }))\n );\n }\n\n if (manifest.autoVisibility !== false) {\n onVisibilityChange(true);\n }\n}\n\nexport async function playMusic(fadeIn?: number): Promise<void> {\n await _playMusic(fadeIn ?? musicFadeIn);\n}\n\nexport function destroy(): void {\n destroyMusic();\n destroySfx();\n closeCtx();\n}\n\nexport {\n _loadMusic as loadMusic,\n _pauseMusic as pauseMusic,\n _stopMusic as stopMusic,\n _setMusicVolume as setMusicVolume,\n _setMusicMuted as setMusicMuted,\n _isMusicMuted as isMusicMuted,\n _isMusicPlaying as isMusicPlaying,\n _toggleMusicMute as toggleMusicMute,\n _loadSfx as loadSfx,\n _playSfx as playSfx,\n _setSfxVolume as setSfxVolume,\n _setSfxGlobalVolume as setSfxGlobalVolume,\n _setSfxMuted as setSfxMuted,\n _isSfxMuted as isSfxMuted,\n _toggleSfxMute as toggleSfxMute,\n onVisibilityChange as handleVisibility,\n};\n\nexport type { AudioManifest, MusicConfig, SfxConfig, VisibilityOptions } from './types.js';\n"],"mappings":"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","names":["ctx","ctx","playMusic"]}
|
package/dist/music.d.ts
CHANGED
|
@@ -3,7 +3,9 @@ export declare function loadMusic(url: string, opts?: {
|
|
|
3
3
|
fadeIn?: number;
|
|
4
4
|
}): Promise<void>;
|
|
5
5
|
export declare function playMusic(fadeIn?: number): Promise<void>;
|
|
6
|
+
/** Stop playback, remembering the playhead so playMusic() resumes from it. */
|
|
6
7
|
export declare function pauseMusic(): void;
|
|
8
|
+
/** End playback. Unlike pauseMusic(), the next play starts from the top. */
|
|
7
9
|
export declare function stopMusic(): void;
|
|
8
10
|
export declare function setMusicVolume(volume: number, rampTime?: number): void;
|
|
9
11
|
export declare function setMusicMuted(muted: boolean, rampTime?: number): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magmacrunch/adenosine-audio",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Web Audio API engine for game music and sound effects",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
},
|
|
18
18
|
"repository": {
|
|
19
19
|
"type": "git",
|
|
20
|
-
"url": "git+https://github.com/
|
|
20
|
+
"url": "git+https://github.com/magmacrunch-media/adenosine.git",
|
|
21
21
|
"directory": "packages/audio"
|
|
22
22
|
},
|
|
23
23
|
"author": "magmacrunch media",
|