diff --git a/packages/frontend/assets/drop-and-fusion/bgm_1.mp3 b/packages/frontend/assets/drop-and-fusion/bgm_1.mp3
new file mode 100644
index 0000000000..cafc34ad9c
Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/bgm_1.mp3 differ
diff --git a/packages/frontend/assets/drop-and-fusion/bubble2.mp3 b/packages/frontend/assets/drop-and-fusion/bubble2.mp3
new file mode 100644
index 0000000000..8b4f8df6e9
Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/bubble2.mp3 differ
diff --git a/packages/frontend/assets/drop-and-fusion/poi1.mp3 b/packages/frontend/assets/drop-and-fusion/poi1.mp3
new file mode 100644
index 0000000000..59dae90965
Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/poi1.mp3 differ
diff --git a/packages/frontend/assets/drop-and-fusion/poi2.mp3 b/packages/frontend/assets/drop-and-fusion/poi2.mp3
new file mode 100644
index 0000000000..a65c653891
Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/poi2.mp3 differ
diff --git a/packages/frontend/src/pages/drop-and-fusion.vue b/packages/frontend/src/pages/drop-and-fusion.vue
index 1daf9ddc62..0ddee55f5f 100644
--- a/packages/frontend/src/pages/drop-and-fusion.vue
+++ b/packages/frontend/src/pages/drop-and-fusion.vue
@@ -103,9 +103,23 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</div>
 				<div :class="[$style.frame]" style="margin-left: auto;">
 					<div :class="$style.frameInner" style="text-align: center;">
+						<div @click="showConfig = !showConfig"><i class="ti ti-settings"></i></div>
 					</div>
 				</div>
 			</div>
+			<div v-if="showConfig" :class="$style.frame">
+				<div :class="$style.frameInner">
+					<MkRange v-model="bgmVolume" :min="0" :max="1" :step="0.0025" :textConverter="(v) => `${Math.floor(v * 100)}%`" :continuousUpdate="true">
+						<template #label>BGM {{ i18n.ts.volume }}</template>
+					</MkRange>
+				</div>
+			</div>
+			<div v-if="showConfig" :class="$style.frame">
+				<div :class="$style.frameInner">
+					<div>Credit</div>
+					<div>BGM: @ys@misskey.design</div>
+				</div>
+			</div>
 			<div :class="$style.frame">
 				<div :class="$style.frameInner">
 					<MkButton @click="restart">Restart</MkButton>
@@ -117,7 +131,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onDeactivated, ref, shallowRef } from 'vue';
+import { onDeactivated, ref, shallowRef, watch } from 'vue';
 import * as Misskey from 'misskey-js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import MkRippleEffect from '@/components/MkRippleEffect.vue';
@@ -134,6 +148,8 @@ import MkSelect from '@/components/MkSelect.vue';
 import { apiUrl } from '@/config.js';
 import { $i } from '@/account.js';
 import { DropAndFusionGame, Mono } from '@/scripts/drop-and-fusion-engine.js';
+import * as sound from '@/scripts/sound.js';
+import MkRange from '@/components/MkRange.vue';
 
 const containerEl = shallowRef<HTMLElement>();
 const canvasEl = shallowRef<HTMLCanvasElement>();
@@ -381,6 +397,8 @@ const gameMode = ref<'normal' | 'square'>('normal');
 const gameOver = ref(false);
 const gameStarted = ref(false);
 const highScore = ref<number | null>(null);
+const showConfig = ref(false);
+const bgmVolume = ref(0.1);
 
 let game: DropAndFusionGame;
 let containerElRect: DOMRect | null = null;
@@ -493,6 +511,8 @@ function attachGameEvents() {
 	});
 }
 
+let bgmNodes: ReturnType<typeof sound.createSourceNode> = null;
+
 async function start() {
 	try {
 		highScore.value = await misskeyApi('i/registry/get', {
@@ -516,12 +536,29 @@ async function start() {
 		),
 	});
 	attachGameEvents();
-	os.promiseDialog(game.load(), () => {
+	os.promiseDialog(game.load(), async () => {
 		game.start();
 		gameStarted.value = true;
+
+		if (bgmNodes) {
+			bgmNodes.soundSource.stop();
+			bgmNodes = null;
+		}
+		const bgmBuffer = await sound.loadAudio('/client-assets/drop-and-fusion/bgm_1.mp3');
+		if (!bgmBuffer) return;
+		bgmNodes = sound.createSourceNode(bgmBuffer, bgmVolume.value);
+		if (!bgmNodes) return;
+		bgmNodes.soundSource.loop = true;
+		bgmNodes.soundSource.start();
 	});
 }
 
+watch(bgmVolume, (value) => {
+	if (bgmNodes) {
+		bgmNodes.gainNode.gain.value = value;
+	}
+});
+
 function getGameImageDriveFile() {
 	return new Promise<Misskey.entities.DriveFile | null>(res => {
 		const dcanvas = document.createElement('canvas');
diff --git a/packages/frontend/src/scripts/drop-and-fusion-engine.ts b/packages/frontend/src/scripts/drop-and-fusion-engine.ts
index 7241525a38..b6e735ddf2 100644
--- a/packages/frontend/src/scripts/drop-and-fusion-engine.ts
+++ b/packages/frontend/src/scripts/drop-and-fusion-engine.ts
@@ -199,7 +199,7 @@ export class DropAndFusionGame extends EventEmitter<{
 		}
 		this.latestFusionedAt = now;
 
-		// TODO: 単に位置だけでなくそれぞれの動きベクトルも融合する
+		// TODO: 単に位置だけでなくそれぞれの動きベクトルも融合する?
 		const newX = (bodyA.position.x + bodyB.position.x) / 2;
 		const newY = (bodyA.position.y + bodyB.position.y) / 2;
 
@@ -222,8 +222,9 @@ export class DropAndFusionGame extends EventEmitter<{
 			const additionalScore = Math.round(currentMono.score * comboBonus);
 			this.score += additionalScore;
 
+			// TODO: 効果音再生はコンポーネント側の責務なので移動する
 			const pan = ((newX / this.gameWidth) - 0.5) * 2;
-			sound.playRaw('syuilo/bubble2', 1, pan, nextMono.sfxPitch);
+			sound.playUrl('/client-assets/drop-and-fusion/bubble2.mp3', 1, pan, nextMono.sfxPitch);
 
 			this.emit('monoAdded', nextMono);
 			this.emit('fusioned', newX, newY, additionalScore);
@@ -234,7 +235,7 @@ export class DropAndFusionGame extends EventEmitter<{
 			//	Matter.Composite.add(world, body);
 			//	bodies.push(body);
 			//}
-			//sound.playRaw({
+			//sound.playUrl({
 			//	type: 'syuilo/bubble2',
 			//	volume: 1,
 			//});
@@ -321,10 +322,11 @@ export class DropAndFusionGame extends EventEmitter<{
 				} else {
 					const energy = pairs.collision.depth;
 					if (energy > minCollisionEnergyForSound) {
+						// TODO: 効果音再生はコンポーネント側の責務なので移動する
 						const vol = (Math.min(maxCollisionEnergyForSound, energy - minCollisionEnergyForSound) / maxCollisionEnergyForSound) / 4;
 						const pan = ((((bodyA.position.x + bodyB.position.x) / 2) / this.gameWidth) - 0.5) * 2;
 						const pitch = soundPitchMin + ((soundPitchMax - soundPitchMin) * (1 - (Math.min(10, energy) / 10)));
-						sound.playRaw('syuilo/poi1', vol, pan, pitch);
+						sound.playUrl('/client-assets/drop-and-fusion/poi1.mp3', vol, pan, pitch);
 					}
 				}
 			}
@@ -382,8 +384,10 @@ export class DropAndFusionGame extends EventEmitter<{
 		this.latestDroppedAt = Date.now();
 		this.emit('dropped');
 		this.emit('monoAdded', st.mono);
+
+		// TODO: 効果音再生はコンポーネント側の責務なので移動する
 		const pan = ((x / this.gameWidth) - 0.5) * 2;
-		sound.playRaw('syuilo/poi2', 1, pan);
+		sound.playUrl('/client-assets/drop-and-fusion/poi2.mp3', 1, pan);
 	}
 
 	public dispose() {
diff --git a/packages/frontend/src/scripts/sound.ts b/packages/frontend/src/scripts/sound.ts
index acde78f5fd..690c342c85 100644
--- a/packages/frontend/src/scripts/sound.ts
+++ b/packages/frontend/src/scripts/sound.ts
@@ -5,7 +5,6 @@
 
 import type { SoundStore } from '@/store.js';
 import { defaultStore } from '@/store.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
 
 let ctx: AudioContext;
 const cache = new Map<string, AudioBuffer>();
@@ -89,69 +88,35 @@ export type OperationType = typeof operationTypes[number];
 
 /**
  * 音声を読み込む
- * @param soundStore サウンド設定
+ * @param url url
  * @param options `useCache`: デフォルトは`true` 一度再生した音声はキャッシュする
  */
-export async function loadAudio(soundStore: {
-	type: Exclude<SoundType, '_driveFile_'>;
-} | {
-	type: '_driveFile_';
-	fileId: string;
-	fileUrl: string;
-}, options?: { useCache?: boolean; }) {
+export async function loadAudio(url: string, options?: { useCache?: boolean; }) {
 	if (_DEV_) console.log('loading audio. opts:', options);
 	// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
-	if (soundStore.type === null || (soundStore.type === '_driveFile_' && !soundStore.fileUrl)) {
-		return;
-	}
-	// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
 	if (ctx == null) {
 		ctx = new AudioContext();
 	}
 	if (options?.useCache ?? true) {
-		if (soundStore.type === '_driveFile_' && cache.has(soundStore.fileId)) {
+		if (cache.has(url)) {
 			if (_DEV_) console.log('use cache');
-			return cache.get(soundStore.fileId) as AudioBuffer;
-		} else if (cache.has(soundStore.type)) {
-			if (_DEV_) console.log('use cache');
-			return cache.get(soundStore.type) as AudioBuffer;
+			return cache.get(url) as AudioBuffer;
 		}
 	}
 
 	let response: Response;
 
-	if (soundStore.type === '_driveFile_') {
-		try {
-			response = await fetch(soundStore.fileUrl);
-		} catch (err) {
-			try {
-				// URLが変わっている可能性があるのでドライブ側からURLを取得するフォールバック
-				const apiRes = await misskeyApi('drive/files/show', {
-					fileId: soundStore.fileId,
-				});
-				response = await fetch(apiRes.url);
-			} catch (fbErr) {
-				// それでも無理なら諦める
-				return;
-			}
-		}
-	} else {
-		try {
-			response = await fetch(`/client-assets/sounds/${soundStore.type}.mp3`);
-		} catch (err) {
-			return;
-		}
+	try {
+		response = await fetch(url);
+	} catch (err) {
+		return;
 	}
 
 	const arrayBuffer = await response.arrayBuffer();
 	const audioBuffer = await ctx.decodeAudioData(arrayBuffer);
 
 	if (options?.useCache ?? true) {
-		if (soundStore.type === '_driveFile_') {
-			cache.set(soundStore.fileId, audioBuffer);
-		} else {
-			cache.set(soundStore.type, audioBuffer);
-		}
+		cache.set(url, audioBuffer);
 	}
 
 	return audioBuffer;
@@ -180,18 +145,26 @@ export function play(operationType: OperationType) {
  * @param soundStore サウンド設定
  */
 export async function playFile(soundStore: SoundStore) {
-	const buffer = await loadAudio(soundStore);
+	if (soundStore.type === null || (soundStore.type === '_driveFile_' && !soundStore.fileUrl)) {
+		return;
+	}
+	const url = soundStore.type === '_driveFile_' ? soundStore.fileUrl : `/client-assets/sounds/${soundStore.type}.mp3`;
+	const buffer = await loadAudio(url);
 	if (!buffer) return;
-	createSourceNode(buffer, soundStore.volume)?.start();
+	createSourceNode(buffer, soundStore.volume)?.soundSource.start();
 }
 
-export async function playRaw(type: Exclude<SoundType, '_driveFile_'>, volume = 1, pan = 0, playbackRate = 1) {
-	const buffer = await loadAudio({ type });
+export async function playUrl(url: string, volume = 1, pan = 0, playbackRate = 1) {
+	const buffer = await loadAudio(url);
 	if (!buffer) return;
-	createSourceNode(buffer, volume, pan, playbackRate)?.start();
+	createSourceNode(buffer, volume, pan, playbackRate)?.soundSource.start();
 }
 
-export function createSourceNode(buffer: AudioBuffer, volume: number, pan = 0, playbackRate = 1) : AudioBufferSourceNode | null {
+export function createSourceNode(buffer: AudioBuffer, volume: number, pan = 0, playbackRate = 1): {
+	soundSource: AudioBufferSourceNode;
+	panNode: StereoPannerNode;
+	gainNode: GainNode;
+} | null {
 	const masterVolume = defaultStore.state.sound_masterVolume;
 	if (isMute() || masterVolume === 0 || volume === 0) {
 		return null;
@@ -211,7 +184,7 @@ export function createSourceNode(buffer: AudioBuffer, volume: number, pan = 0, p
 		.connect(gainNode)
 		.connect(ctx.destination);
 
-	return soundSource;
+	return { soundSource, panNode, gainNode };
 }
 
 /**
diff --git a/packages/frontend/src/widgets/WidgetJobQueue.vue b/packages/frontend/src/widgets/WidgetJobQueue.vue
index 91983d8474..89ad3bf323 100644
--- a/packages/frontend/src/widgets/WidgetJobQueue.vue
+++ b/packages/frontend/src/widgets/WidgetJobQueue.vue
@@ -104,10 +104,7 @@ const jammedAudioBuffer = ref<AudioBuffer | null>(null);
 const jammedSoundNodePlaying = ref<boolean>(false);
 
 if (defaultStore.state.sound_masterVolume) {
-	sound.loadAudio({
-		type: 'syuilo/queue-jammed',
-		volume: 1,
-	}).then(buf => {
+	sound.loadAudio('/client-assets/sounds/syuilo/queue-jammed.mp3').then(buf => {
 		if (!buf) throw new Error('[WidgetJobQueue] Failed to initialize AudioBuffer');
 		jammedAudioBuffer.value = buf;
 	});
@@ -126,7 +123,7 @@ const onStats = (stats) => {
 		current[domain].delayed = stats[domain].delayed;
 
 		if (current[domain].waiting > 0 && widgetProps.sound && jammedAudioBuffer.value && !jammedSoundNodePlaying.value) {
-			const soundNode = sound.createSourceNode(jammedAudioBuffer.value, 1);
+			const soundNode = sound.createSourceNode(jammedAudioBuffer.value, 1)?.soundSource;
 			if (soundNode) {
 				jammedSoundNodePlaying.value = true;
 				soundNode.onended = () => jammedSoundNodePlaying.value = false;