diff --git a/locales/index.d.ts b/locales/index.d.ts
index 9415661000..1ec9ed64e0 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -1233,6 +1233,8 @@ export interface Locale {
     "addMfmFunction": string;
     "enableQuickAddMfmFunction": string;
     "bubbleGame": string;
+    "sfx": string;
+    "soundWillBePlayed": string;
     "_announcement": {
         "forExistingUsers": string;
         "forExistingUsersDescription": string;
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 44430bc0af..4a4feed061 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1230,6 +1230,8 @@ decorate: "デコる"
 addMfmFunction: "装飾を追加"
 enableQuickAddMfmFunction: "高度なMFMのピッカーを表示する"
 bubbleGame: "バブルゲーム"
+sfx: "効果音"
+soundWillBePlayed: "サウンドが再生されます"
 
 _announcement:
   forExistingUsers: "既存ユーザーのみ"
diff --git a/packages/frontend/assets/drop-and-fusion/click.mp3 b/packages/frontend/assets/drop-and-fusion/click.mp3
new file mode 100644
index 0000000000..ef03e60f61
Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/click.mp3 differ
diff --git a/packages/frontend/assets/drop-and-fusion/hold.mp3 b/packages/frontend/assets/drop-and-fusion/hold.mp3
new file mode 100644
index 0000000000..f064c976d3
Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/hold.mp3 differ
diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts
index 81e0f3ae92..f248bc10e4 100644
--- a/packages/frontend/src/boot/main-boot.ts
+++ b/packages/frontend/src/boot/main-boot.ts
@@ -269,7 +269,7 @@ export async function mainBoot() {
 
 		main.on('unreadAntenna', () => {
 			updateAccount({ hasUnreadAntenna: true });
-			sound.play('antenna');
+			sound.playMisskeySfx('antenna');
 		});
 
 		main.on('readAllAnnouncements', () => {
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index 7870e1e4b8..182ef7661e 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -563,7 +563,7 @@ function reply(viaKeyboard = false): void {
 function like(): void {
 	pleaseLogin();
 	showMovedDialog();
-	sound.play('reaction');
+	sound.playMisskeySfx('reaction');
 	if (props.mock) {
 		return;
 	}
@@ -584,7 +584,7 @@ function react(viaKeyboard = false): void {
 	pleaseLogin();
 	showMovedDialog();
 	if (appearNote.value.reactionAcceptance === 'likeOnly') {
-		sound.play('reaction');
+		sound.playMisskeySfx('reaction');
 
 		if (props.mock) {
 			return;
@@ -604,7 +604,7 @@ function react(viaKeyboard = false): void {
 	} else {
 		blur();
 		reactionPicker.show(reactButton.value, reaction => {
-			sound.play('reaction');
+			sound.playMisskeySfx('reaction');
 
 			if (props.mock) {
 				emit('reaction', reaction);
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index 317666a5e2..74fb476f1a 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -597,6 +597,8 @@ function react(viaKeyboard = false): void {
 	pleaseLogin();
 	showMovedDialog();
 	if (appearNote.value.reactionAcceptance === 'likeOnly') {
+		sound.playMisskeySfx('reaction');
+
 		misskeyApi('notes/like', {
 			noteId: appearNote.value.id,
 			override: defaultLike.value,
@@ -611,7 +613,7 @@ function react(viaKeyboard = false): void {
 	} else {
 		blur();
 		reactionPicker.show(reactButton.value, reaction => {
-			sound.play('reaction');
+			sound.playMisskeySfx('reaction');
 
 			misskeyApi('notes/reactions/create', {
 				noteId: appearNote.value.id,
@@ -629,7 +631,7 @@ function react(viaKeyboard = false): void {
 function like(): void {
 	pleaseLogin();
 	showMovedDialog();
-	sound.play('reaction');
+	sound.playMisskeySfx('reaction');
 	misskeyApi('notes/like', {
 		noteId: appearNote.value.id,
 		override: defaultLike.value,
diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue
index 9d403bf09e..fbc7a7f9ea 100644
--- a/packages/frontend/src/components/MkNoteSub.vue
+++ b/packages/frontend/src/components/MkNoteSub.vue
@@ -195,7 +195,7 @@ function reply(viaKeyboard = false): void {
 function react(viaKeyboard = false): void {
 	pleaseLogin();
 	showMovedDialog();
-	sound.play('reaction');
+	sound.playMisskeySfx('reaction');
 	if (props.note.reactionAcceptance === 'likeOnly') {
 		misskeyApi('notes/like', {
 			noteId: props.note.id,
@@ -227,7 +227,7 @@ function react(viaKeyboard = false): void {
 function like(): void {
 	pleaseLogin();
 	showMovedDialog();
-	sound.play('reaction');
+	sound.playMisskeySfx('reaction');
 	misskeyApi('notes/like', {
 		noteId: props.note.id,
 		override: defaultLike.value,
diff --git a/packages/frontend/src/components/MkRange.vue b/packages/frontend/src/components/MkRange.vue
index c1f5b6a790..e8760e1946 100644
--- a/packages/frontend/src/components/MkRange.vue
+++ b/packages/frontend/src/components/MkRange.vue
@@ -43,6 +43,7 @@ const props = withDefaults(defineProps<{
 
 const emit = defineEmits<{
 	(ev: 'update:modelValue', value: number): void;
+	(ev: 'dragEnded', value: number): void;
 }>();
 
 const containerEl = shallowRef<HTMLElement>();
@@ -143,6 +144,7 @@ const onMousedown = (ev: MouseEvent | TouchEvent) => {
 		// 値が変わってたら通知
 		if (beforeValue !== finalValue.value) {
 			emit('update:modelValue', finalValue.value);
+			emit('dragEnded', finalValue.value);
 		}
 	};
 
diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
index a3791aee07..c8c8d0f910 100644
--- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
@@ -62,7 +62,7 @@ async function toggleReaction() {
 		if (confirm.canceled) return;
 
 		if (oldReaction !== props.reaction) {
-			sound.play('reaction');
+			sound.playMisskeySfx('reaction');
 		}
 
 		if (mock) {
@@ -81,7 +81,7 @@ async function toggleReaction() {
 			}
 		});
 	} else {
-		sound.play('reaction');
+		sound.playMisskeySfx('reaction');
 
 		if (mock) {
 			emit('reactionToggled', props.reaction, (props.count + 1));
diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue
index 00be5d2042..572d6edcdd 100644
--- a/packages/frontend/src/components/MkTimeline.vue
+++ b/packages/frontend/src/components/MkTimeline.vue
@@ -84,7 +84,7 @@ function prepend(note) {
 	emit('note');
 
 	if (props.sound) {
-		sound.play($i && (note.userId === $i.id) ? 'noteMy' : 'note');
+		sound.playMisskeySfx($i && (note.userId === $i.id) ? 'noteMy' : 'note');
 	}
 }
 
diff --git a/packages/frontend/src/components/SkNote.vue b/packages/frontend/src/components/SkNote.vue
index 66ef22633d..15192405f5 100644
--- a/packages/frontend/src/components/SkNote.vue
+++ b/packages/frontend/src/components/SkNote.vue
@@ -564,7 +564,7 @@ function reply(viaKeyboard = false): void {
 function like(): void {
 	pleaseLogin();
 	showMovedDialog();
-	sound.play('reaction');
+	sound.playMisskeySfx('reaction');
 	if (props.mock) {
 		return;
 	}
@@ -585,7 +585,7 @@ function react(viaKeyboard = false): void {
 	pleaseLogin();
 	showMovedDialog();
 	if (appearNote.value.reactionAcceptance === 'likeOnly') {
-		sound.play('reaction');
+		sound.playMisskeySfx('reaction');
 
 		if (props.mock) {
 			return;
@@ -605,7 +605,7 @@ function react(viaKeyboard = false): void {
 	} else {
 		blur();
 		reactionPicker.show(reactButton.value, reaction => {
-			sound.play('reaction');
+			sound.playMisskeySfx('reaction');
 
 			if (props.mock) {
 				emit('reaction', reaction);
diff --git a/packages/frontend/src/components/SkNoteDetailed.vue b/packages/frontend/src/components/SkNoteDetailed.vue
index 212fa99bef..014c655bb8 100644
--- a/packages/frontend/src/components/SkNoteDetailed.vue
+++ b/packages/frontend/src/components/SkNoteDetailed.vue
@@ -620,7 +620,7 @@ function react(viaKeyboard = false): void {
 	} else {
 		blur();
 		reactionPicker.show(reactButton.value, reaction => {
-			sound.play('reaction');
+			sound.playMisskeySfx('reaction');
 
 			misskeyApi('notes/reactions/create', {
 				noteId: appearNote.value.id,
@@ -638,7 +638,7 @@ function react(viaKeyboard = false): void {
 function like(): void {
 	pleaseLogin();
 	showMovedDialog();
-	sound.play('reaction');
+	sound.playMisskeySfx('reaction');
 	misskeyApi('notes/like', {
 		noteId: appearNote.value.id,
 		override: defaultLike.value,
diff --git a/packages/frontend/src/components/SkNoteSub.vue b/packages/frontend/src/components/SkNoteSub.vue
index 60a574731f..363dcef348 100644
--- a/packages/frontend/src/components/SkNoteSub.vue
+++ b/packages/frontend/src/components/SkNoteSub.vue
@@ -204,7 +204,7 @@ function reply(viaKeyboard = false): void {
 function react(viaKeyboard = false): void {
 	pleaseLogin();
 	showMovedDialog();
-	sound.play('reaction');
+	sound.playMisskeySfx('reaction');
 	if (props.note.reactionAcceptance === 'likeOnly') {
 		misskeyApi('notes/like', {
 			noteId: props.note.id,
@@ -236,7 +236,7 @@ function react(viaKeyboard = false): void {
 function like(): void {
 	pleaseLogin();
 	showMovedDialog();
-	sound.play('reaction');
+	sound.playMisskeySfx('reaction');
 	misskeyApi('notes/like', {
 		noteId: props.note.id,
 		override: defaultLike.value,
diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue
index e8732d1b16..18fdcd4ffb 100644
--- a/packages/frontend/src/components/global/MkCustomEmoji.vue
+++ b/packages/frontend/src/components/global/MkCustomEmoji.vue
@@ -91,7 +91,7 @@ function onClick(ev: MouseEvent) {
 			icon: 'ph-smiley ph-bold ph-lg',
 			action: () => {
 				react(`:${props.name}:`);
-				sound.play('reaction');
+				sound.playMisskeySfx('reaction');
 			},
 		}] : [])], ev.currentTarget ?? ev.target);
 	}
diff --git a/packages/frontend/src/components/global/MkEmoji.vue b/packages/frontend/src/components/global/MkEmoji.vue
index 8945f2b64b..ba4a40ce08 100644
--- a/packages/frontend/src/components/global/MkEmoji.vue
+++ b/packages/frontend/src/components/global/MkEmoji.vue
@@ -55,7 +55,7 @@ function onClick(ev: MouseEvent) {
 			icon: 'ph-smiley ph-bold ph-lg',
 			action: () => {
 				react(props.emoji);
-				sound.play('reaction');
+				sound.playMisskeySfx('reaction');
 			},
 		}] : [])], ev.currentTarget ?? ev.target);
 	}
diff --git a/packages/frontend/src/pages/drop-and-fusion.vue b/packages/frontend/src/pages/drop-and-fusion.vue
index 0ddee55f5f..b8d3d8bf04 100644
--- a/packages/frontend/src/pages/drop-and-fusion.vue
+++ b/packages/frontend/src/pages/drop-and-fusion.vue
@@ -24,20 +24,31 @@ SPDX-License-Identifier: AGPL-3.0-only
 							<MkButton primary gradate large rounded inline @click="start">{{ i18n.ts.start }}</MkButton>
 						</div>
 					</div>
+					<div :class="$style.frameInner">
+						<div class="_gaps" style="padding: 16px;">
+							<div style="font-size: 90%;"><i class="ti ti-music"></i> {{ i18n.ts.soundWillBePlayed }}</div>
+							<MkSwitch v-model="mute">
+								<template #label>{{ i18n.ts.mute }}</template>
+							</MkSwitch>
+						</div>
+					</div>
 				</div>
 			</div>
 		</div>
 		<div v-show="gameStarted" class="_gaps_s" :class="$style.root">
-			<div style="display: flex;">
-				<div :class="$style.frame" style="flex: 1; margin-right: 10px;">
+			<div :class="$style.header">
+				<div :class="[$style.frame, $style.headerTitle]">
 					<div :class="$style.frameInner">
 						<b>BUBBLE GAME</b>
 						<div>- {{ gameMode }} -</div>
 					</div>
 				</div>
-				<div :class="[$style.frame, $style.stock]" style="margin-left: auto;">
-					<div :class="$style.frameInner" style="text-align: center;">
-						NEXT >>>
+				<div :class="[$style.frame, $style.frameH]">
+					<div :class="$style.frameInner">
+						<MkButton inline small @click="hold">HOLD</MkButton>
+						<img v-if="holdingStock" :src="game.getTextureImageUrl(holdingStock.mono)" style="width: 32px; margin-left: 8px; vertical-align: bottom;"/>
+					</div>
+					<div :class="[$style.frameInner, $style.stock]" style="text-align: center;">
 						<TransitionGroup
 							:enterActiveClass="$style.transition_stock_enterActive"
 							:leaveActiveClass="$style.transition_stock_leaveActive"
@@ -45,28 +56,26 @@ SPDX-License-Identifier: AGPL-3.0-only
 							:leaveToClass="$style.transition_stock_leaveTo"
 							:moveClass="$style.transition_stock_move"
 						>
-							<div v-for="x in stock" :key="x.id" style="display: inline-block;">
-								<img :src="game.getTextureImageUrl(x.mono)" style="width: 32px;"/>
-							</div>
+							<img v-for="x in stock" :key="x.id" :src="game.getTextureImageUrl(x.mono)" style="width: 32px; vertical-align: bottom;"/>
 						</TransitionGroup>
 					</div>
 				</div>
 			</div>
-			<div :class="$style.main" @contextmenu.stop.prevent>
-				<div ref="containerEl" :class="[$style.container, { [$style.gameOver]: gameOver }]" @click.stop.prevent="onClick" @touchmove.stop.prevent="onTouchmove" @touchend="onTouchend" @mousemove="onMousemove">
-					<img v-if="defaultStore.state.darkMode" src="/client-assets/drop-and-fusion/frame-dark.svg" :class="$style.mainFrameImg"/>
-					<img v-else src="/client-assets/drop-and-fusion/frame-light.svg" :class="$style.mainFrameImg"/>
-					<canvas ref="canvasEl" :class="$style.canvas"/>
-					<Transition
-						:enterActiveClass="$style.transition_combo_enterActive"
-						:leaveActiveClass="$style.transition_combo_leaveActive"
-						:enterFromClass="$style.transition_combo_enterFrom"
-						:leaveToClass="$style.transition_combo_leaveTo"
-						:moveClass="$style.transition_combo_move"
-					>
-						<div v-show="combo > 1" :class="$style.combo" :style="{ fontSize: `${100 + ((comboPrev - 2) * 15)}%` }">{{ comboPrev }} Chain!</div>
-					</Transition>
-					<img v-if="currentPick" src="/client-assets/drop-and-fusion/dropper.png" :class="$style.dropper" :style="{ left: dropperX + 'px' }"/>
+			<div ref="containerEl" :class="[$style.gameContainer, { [$style.gameOver]: gameOver }]" @contextmenu.stop.prevent @click.stop.prevent="onClick" @touchmove.stop.prevent="onTouchmove" @touchend="onTouchend" @mousemove="onMousemove">
+				<img v-if="defaultStore.state.darkMode" src="/client-assets/drop-and-fusion/frame-dark.svg" :class="$style.mainFrameImg"/>
+				<img v-else src="/client-assets/drop-and-fusion/frame-light.svg" :class="$style.mainFrameImg"/>
+				<canvas ref="canvasEl" :class="$style.canvas"/>
+				<Transition
+					:enterActiveClass="$style.transition_combo_enterActive"
+					:leaveActiveClass="$style.transition_combo_leaveActive"
+					:enterFromClass="$style.transition_combo_enterFrom"
+					:leaveToClass="$style.transition_combo_leaveTo"
+					:moveClass="$style.transition_combo_move"
+				>
+					<div v-show="combo > 1" :class="$style.combo" :style="{ fontSize: `${100 + ((comboPrev - 2) * 15)}%` }">{{ comboPrev }} Chain!</div>
+				</Transition>
+				<div :class="$style.dropperContainer" :style="{ left: dropperX + 'px' }">
+					<!--<img v-if="currentPick" src="/client-assets/drop-and-fusion/dropper.png" :class="$style.dropper" :style="{ left: dropperX + 'px' }"/>-->
 					<Transition
 						:enterActiveClass="$style.transition_picked_enterActive"
 						:leaveActiveClass="$style.transition_picked_leaveActive"
@@ -75,21 +84,21 @@ SPDX-License-Identifier: AGPL-3.0-only
 						:moveClass="$style.transition_picked_move"
 						mode="out-in"
 					>
-						<img v-if="currentPick" :key="currentPick.id" :src="game.getTextureImageUrl(currentPick.mono)" :class="$style.currentMono" :style="{ top: -(currentPick?.mono.size / 2) + 'px', left: (dropperX - (currentPick?.mono.size / 2)) + 'px', width: `${currentPick?.mono.size}px` }"/>
+						<img v-if="currentPick" :key="currentPick.id" :src="game.getTextureImageUrl(currentPick.mono)" :class="$style.currentMono" :style="{ marginBottom: -((currentPick?.mono.size * viewScale) / 2) + 'px', left: -((currentPick?.mono.size * viewScale) / 2) + 'px', width: `${currentPick?.mono.size * viewScale}px` }"/>
 					</Transition>
 					<template v-if="dropReady && currentPick">
-						<img src="/client-assets/drop-and-fusion/drop-arrow.svg" :class="$style.currentMonoArrow" :style="{ top: (currentPick.mono.size / 2) + 10 + 'px', left: (dropperX - 10) + 'px', width: `20px` }"/>
-						<div :class="$style.dropGuide" :style="{ left: (dropperX - 2) + 'px' }"/>
+						<img src="/client-assets/drop-and-fusion/drop-arrow.svg" :class="$style.currentMonoArrow"/>
+						<div :class="$style.dropGuide"/>
 					</template>
-					<div v-if="gameOver" :class="$style.gameOverLabel">
-						<div class="_gaps_s">
-							<img src="/client-assets/drop-and-fusion/gameover.png" style="width: 200px; max-width: 100%; display: block; margin: auto; margin-bottom: -5px;"/>
-							<div>SCORE: <MkNumber :value="score"/></div>
-							<div>MAX CHAIN: <MkNumber :value="maxCombo"/></div>
-							<div class="_buttonsCenter">
-								<MkButton primary rounded @click="restart">Restart</MkButton>
-								<MkButton primary rounded @click="share">Share</MkButton>
-							</div>
+				</div>
+				<div v-if="gameOver" :class="$style.gameOverLabel">
+					<div class="_gaps_s">
+						<img src="/client-assets/drop-and-fusion/gameover.png" style="width: 200px; max-width: 100%; display: block; margin: auto; margin-bottom: -5px;"/>
+						<div>SCORE: <MkNumber :value="score"/></div>
+						<div>MAX CHAIN: <MkNumber :value="maxCombo"/></div>
+						<div class="_buttonsCenter">
+							<MkButton primary rounded @click="restart">Restart</MkButton>
+							<MkButton primary rounded @click="share">Share</MkButton>
 						</div>
 					</div>
 				</div>
@@ -109,15 +118,23 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</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 class="_gaps">
+						<MkRange v-model="bgmVolume" :min="0" :max="1" :step="0.01" :textConverter="(v) => `${Math.floor(v * 100)}%`" :continuousUpdate="true" @dragEnded="(v) => updateSettings('bgmVolume', v)">
+							<template #label>BGM {{ i18n.ts.volume }}</template>
+						</MkRange>
+						<MkRange v-model="sfxVolume" :min="0" :max="1" :step="0.01" :textConverter="(v) => `${Math.floor(v * 100)}%`" :continuousUpdate="true" @dragEnded="(v) => updateSettings('sfxVolume', v)">
+							<template #label>{{ i18n.ts.sfx }} {{ i18n.ts.volume }}</template>
+						</MkRange>
+					</div>
 				</div>
-			</div>
-			<div v-if="showConfig" :class="$style.frame">
 				<div :class="$style.frameInner">
-					<div>Credit</div>
-					<div>BGM: @ys@misskey.design</div>
+					<div class="_gaps_s">
+						<div><b>Credit</b></div>
+						<div>
+							<div>Ai-chan illustration: @poteriri@misskey.io</div>
+							<div>BGM: @ys@misskey.design</div>
+						</div>
+					</div>
 				</div>
 			</div>
 			<div :class="$style.frame">
@@ -150,10 +167,7 @@ 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>();
-const dropperX = ref(0);
+import MkSwitch from '@/components/MkSwitch.vue';
 
 const NORMAL_BASE_SIZE = 30;
 const NORAML_MONOS: Mono[] = [{
@@ -384,10 +398,16 @@ const SQUARE_MONOS: Mono[] = [{
 const GAME_WIDTH = 450;
 const GAME_HEIGHT = 600;
 
-let viewScaleX = 1;
-let viewScaleY = 1;
+let viewScale = 1;
+let game: DropAndFusionGame;
+let containerElRect: DOMRect | null = null;
+
+const containerEl = shallowRef<HTMLElement>();
+const canvasEl = shallowRef<HTMLCanvasElement>();
+const dropperX = ref(0);
 const currentPick = shallowRef<{ id: string; mono: Mono } | null>(null);
 const stock = shallowRef<{ id: string; mono: Mono }[]>([]);
+const holdingStock = shallowRef<{ id: string; mono: Mono } | null>(null);
 const score = ref(0);
 const combo = ref(0);
 const comboPrev = ref(0);
@@ -398,20 +418,19 @@ 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;
+const mute = ref(false);
+const bgmVolume = ref(defaultStore.state.dropAndFusion.bgmVolume);
+const sfxVolume = ref(defaultStore.state.dropAndFusion.sfxVolume);
 
 function onClick(ev: MouseEvent) {
 	if (!containerElRect) return;
-	const x = (ev.clientX - containerElRect.left) / viewScaleX;
+	const x = (ev.clientX - containerElRect.left) / viewScale;
 	game.drop(x);
 }
 
 function onTouchend(ev: TouchEvent) {
 	if (!containerElRect) return;
-	const x = (ev.changedTouches[0].clientX - containerElRect.left) / viewScaleX;
+	const x = (ev.changedTouches[0].clientX - containerElRect.left) / viewScale;
 	game.drop(x);
 }
 
@@ -431,6 +450,10 @@ function moveDropper(rect: DOMRect, x: number) {
 	dropperX.value = Math.min(rect.width * ((GAME_WIDTH - game.PLAYAREA_MARGIN) / GAME_WIDTH), Math.max(rect.width * (game.PLAYAREA_MARGIN / GAME_WIDTH), x));
 }
 
+function hold() {
+	game.hold();
+}
+
 function restart() {
 	game.dispose();
 	gameOver.value = false;
@@ -440,6 +463,7 @@ function restart() {
 	score.value = 0;
 	combo.value = 0;
 	comboPrev.value = 0;
+	bgmNodes?.soundSource.stop();
 	gameStarted.value = false;
 }
 
@@ -463,6 +487,10 @@ function attachGameEvents() {
 		stock.value = JSON.parse(JSON.stringify(value.slice(1)));
 	});
 
+	game.addListener('changeHolding', value => {
+		holdingStock.value = value;
+	});
+
 	game.addListener('dropped', () => {
 		dropReady.value = false;
 		window.setTimeout(() => {
@@ -476,8 +504,8 @@ function attachGameEvents() {
 		if (!canvasEl.value) return;
 
 		const rect = canvasEl.value.getBoundingClientRect();
-		const domX = rect.left + (x * viewScaleX);
-		const domY = rect.top + (y * viewScaleY);
+		const domX = rect.left + (x * viewScale);
+		const domY = rect.top + (y * viewScale);
 		os.popup(MkRippleEffect, { x: domX, y: domY }, {}, 'end');
 		os.popup(MkPlusOneEffect, { x: domX, y: domY, value: scoreDelta }, {}, 'end');
 	});
@@ -511,7 +539,7 @@ function attachGameEvents() {
 	});
 }
 
-let bgmNodes: ReturnType<typeof sound.createSourceNode> = null;
+let bgmNodes: ReturnType<typeof sound.createSourceNode> | null = null;
 
 async function start() {
 	try {
@@ -527,6 +555,7 @@ async function start() {
 		width: GAME_WIDTH,
 		height: GAME_HEIGHT,
 		canvas: canvasEl.value!,
+		sfxVolume: mute.value ? 0 : sfxVolume.value,
 		...(
 			gameMode.value === 'normal' ? {
 				monoDefinitions: NORAML_MONOS,
@@ -546,19 +575,50 @@ async function start() {
 		}
 		const bgmBuffer = await sound.loadAudio('/client-assets/drop-and-fusion/bgm_1.mp3');
 		if (!bgmBuffer) return;
-		bgmNodes = sound.createSourceNode(bgmBuffer, bgmVolume.value);
+		bgmNodes = sound.createSourceNode(bgmBuffer, {
+			volume: mute.value ? 0 : bgmVolume.value,
+		});
 		if (!bgmNodes) return;
 		bgmNodes.soundSource.loop = true;
 		bgmNodes.soundSource.start();
 	});
 }
 
-watch(bgmVolume, (value) => {
+watch(bgmVolume, (newValue, oldValue) => {
 	if (bgmNodes) {
-		bgmNodes.gainNode.gain.value = value;
+		bgmNodes.gainNode.gain.value = mute.value ? 0 : newValue;
 	}
 });
 
+watch(sfxVolume, (newValue, oldValue) => {
+	// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+	if (game) {
+		game.setSfxVolume(mute.value ? 0 : newValue);
+	}
+});
+
+function updateSettings<
+	K extends keyof typeof defaultStore.state.dropAndFusion,
+	V extends typeof defaultStore.state.dropAndFusion[K],
+>(key: K, value: V) {
+	const changes: { [P in K]?: V } = {};
+	changes[key] = value;
+	defaultStore.set('dropAndFusion', {
+		...defaultStore.state.dropAndFusion,
+		...changes,
+	});
+}
+
+function loadImage(url: string) {
+	return new Promise<HTMLImageElement>(res => {
+		const img = new Image();
+		img.src = url;
+		img.addEventListener('load', () => {
+			res(img);
+		});
+	});
+}
+
 function getGameImageDriveFile() {
 	return new Promise<Misskey.entities.DriveFile | null>(res => {
 		const dcanvas = document.createElement('canvas');
@@ -566,13 +626,18 @@ function getGameImageDriveFile() {
 		dcanvas.height = GAME_HEIGHT;
 		const ctx = dcanvas.getContext('2d');
 		if (!ctx || !canvasEl.value) return res(null);
-		const dimage = new Image();
-		dimage.src = '/client-assets/drop-and-fusion/frame-light.svg';
-		dimage.addEventListener('load', () => {
+		Promise.all([
+			loadImage('/client-assets/drop-and-fusion/frame-light.svg'),
+			loadImage('/client-assets/drop-and-fusion/logo.png'),
+		]).then((images) => {
+			const [frame, logo] = images;
 			ctx.fillStyle = '#fff';
 			ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
-			ctx.drawImage(dimage, 0, 0, GAME_WIDTH, GAME_HEIGHT);
+			ctx.drawImage(frame, 0, 0, GAME_WIDTH, GAME_HEIGHT);
 			ctx.drawImage(canvasEl.value!, 0, 0, GAME_WIDTH, GAME_HEIGHT);
+			ctx.globalAlpha = 0.7;
+			ctx.drawImage(logo, GAME_WIDTH * 0.55, 6, GAME_WIDTH * 0.45, GAME_WIDTH * 0.45 * (logo.height / logo.width));
+			ctx.globalAlpha = 1;
 
 			dcanvas.toBlob(blob => {
 				if (!blob) return res(null);
@@ -610,22 +675,22 @@ async function share() {
 	os.post({
 		initialText: `#BubbleGame
 MODE: ${gameMode.value}
-SCORE: ${score.value} (MAX CHAIN: ${maxCombo.value})})`,
+SCORE: ${score.value} (MAX CHAIN: ${maxCombo.value})`,
 		initialFiles: [file],
+		instant: true,
 	});
 }
 
 useInterval(() => {
 	if (!canvasEl.value) return;
 	const actualCanvasWidth = canvasEl.value.getBoundingClientRect().width;
-	const actualCanvasHeight = canvasEl.value.getBoundingClientRect().height;
-	viewScaleX = actualCanvasWidth / GAME_WIDTH;
-	viewScaleY = actualCanvasHeight / GAME_HEIGHT;
+	if (actualCanvasWidth === 0) return;
+	viewScale = actualCanvasWidth / GAME_WIDTH;
 	containerElRect = containerEl.value?.getBoundingClientRect() ?? null;
 }, 1000, { immediate: false, afterMounted: true });
 
 onDeactivated(() => {
-	game.dispose();
+	restart();
 });
 
 definePageMetadata({
@@ -697,16 +762,52 @@ definePageMetadata({
 	box-shadow: 0 6px 16px #0007, 0 0 1px 1px #693410, inset 0 0 2px 1px #ce8a5c;
 	border-radius: 10px;
 }
+
+.frameH {
+	display: flex;
+	gap: 6px;
+}
+
 .frameInner {
-	padding: 4px 8px;
+	padding: 8px;
+	margin-top: 8px;
 	background: #F1E8DC;
 	box-shadow: 0 0 2px 1px #ce8a5c, inset 0 0 1px 1px #693410;
 	border-radius: 6px;
 	color: #693410;
+
+	&:first-child {
+		margin-top: 0;
+	}
 }
 
-.main {
+.frameDivider {
+	height: 0;
+	border: none;
+	border-top: 1px solid #693410;
+	border-bottom: 1px solid #ce8a5c;
+}
+
+.header {
 	position: relative;
+	z-index: 10;
+	display: grid;
+	grid-template-columns: 1fr;
+	grid-template-rows: auto auto;
+	gap: 8px;
+
+	> .headerTitle {
+		text-align: center;
+	}
+
+	@media (min-width: 500px) {
+		grid-template-columns: 1fr auto;
+		grid-template-rows: auto;
+
+		> .headerTitle {
+			text-align: start;
+		}
+	}
 }
 
 .mainFrameImg {
@@ -724,15 +825,15 @@ definePageMetadata({
 	position: relative;
 	display: block;
 	z-index: 1;
-	margin-top: -50px;
 	width: 100% !important;
 	height: auto !important;
 	pointer-events: none;
 	user-select: none;
 }
 
-.container {
+.gameContainer {
 	position: relative;
+	margin-top: -20px;
 }
 
 .stock {
@@ -755,45 +856,51 @@ definePageMetadata({
 	user-select: none;
 }
 
-.currentMono {
+.dropperContainer {
 	position: absolute;
-	margin-top: 80px;
+	top: 0;
+	height: 100%;
 	z-index: 2;
-	filter: drop-shadow(0 6px 16px #0007);
 	pointer-events: none;
 	user-select: none;
+	will-change: left;
+}
+
+.currentMono {
+	position: absolute;
+	display: block;
+	bottom: 88%;
+	z-index: 2;
+	filter: drop-shadow(0 6px 16px #0007);
 }
 
 .dropper {
-	position: absolute;
+	position: relative;
 	top: 0;
 	width: 70px;
 	margin-top: -10px;
 	margin-left: -30px;
 	z-index: 2;
 	filter: drop-shadow(0 6px 16px #0007);
-	pointer-events: none;
-	user-select: none;
 }
 
 .currentMonoArrow {
 	position: absolute;
-	margin-top: 100px;
+	width: 20px;
+	bottom: 80%;
+	left: -10px;
 	z-index: 3;
 	animation: currentMonoArrow 2s ease infinite;
-	pointer-events: none;
-	user-select: none;
 }
 
 .dropGuide {
 	position: absolute;
-	top: 120px;
 	z-index: 3;
+	bottom: 0;
 	width: 3px;
-	height: calc(100% - 120px);
+	margin-left: -2px;
+	height: 85%;
 	background: #f002;
-	pointer-events: none;
-	user-select: none;
 }
 
 .gameOverLabel {
diff --git a/packages/frontend/src/pages/settings/sounds.sound.vue b/packages/frontend/src/pages/settings/sounds.sound.vue
index 813a971d3f..73812d592a 100644
--- a/packages/frontend/src/pages/settings/sounds.sound.vue
+++ b/packages/frontend/src/pages/settings/sounds.sound.vue
@@ -33,7 +33,7 @@ import MkRange from '@/components/MkRange.vue';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { playFile, soundsTypes, getSoundDuration } from '@/scripts/sound.js';
+import { playMisskeySfxFile, soundsTypes, getSoundDuration } from '@/scripts/sound.js';
 import { selectFile } from '@/scripts/select-file.js';
 
 const props = defineProps<{
@@ -119,7 +119,7 @@ function listen() {
 		return;
 	}
 
-	playFile(type.value === '_driveFile_' ? {
+	playMisskeySfxFile(type.value === '_driveFile_' ? {
 		type: '_driveFile_',
 		fileId: fileId.value as string,
 		fileUrl: fileUrl.value as string,
diff --git a/packages/frontend/src/scripts/drop-and-fusion-engine.ts b/packages/frontend/src/scripts/drop-and-fusion-engine.ts
index b6e735ddf2..f71f3a668e 100644
--- a/packages/frontend/src/scripts/drop-and-fusion-engine.ts
+++ b/packages/frontend/src/scripts/drop-and-fusion-engine.ts
@@ -20,17 +20,17 @@ export type Mono = {
 	spriteScale: number;
 };
 
-const PHYSICS_QUALITY_FACTOR = 16; // 低いほどパフォーマンスが高いがガタガタして安定しなくなる、逆に高すぎても何故か不安定になる
-
 export class DropAndFusionGame extends EventEmitter<{
 	changeScore: (newScore: number) => void;
 	changeCombo: (newCombo: number) => void;
 	changeStock: (newStock: { id: string; mono: Mono }[]) => void;
+	changeHolding: (newHolding: { id: string; mono: Mono } | null) => void;
 	dropped: () => void;
 	fusioned: (x: number, y: number, scoreDelta: number) => void;
 	monoAdded: (mono: Mono) => void;
 	gameOver: () => void;
 }> {
+	private PHYSICS_QUALITY_FACTOR = 16; // 低いほどパフォーマンスが高いがガタガタして安定しなくなる、逆に高すぎても何故か不安定になる
 	private COMBO_INTERVAL = 1000;
 	public readonly DROP_INTERVAL = 500;
 	public readonly PLAYAREA_MARGIN = 25;
@@ -48,6 +48,8 @@ export class DropAndFusionGame extends EventEmitter<{
 	private monoTextures: Record<string, Blob> = {};
 	private monoTextureUrls: Record<string, string> = {};
 
+	private sfxVolume = 1;
+
 	/**
 	 * フィールドに出ていて、かつ合体の対象となるアイテム
 	 */
@@ -58,6 +60,7 @@ export class DropAndFusionGame extends EventEmitter<{
 	private latestDroppedAt = 0;
 	private latestFusionedAt = 0;
 	private stock: { id: string; mono: Mono }[] = [];
+	private holding: { id: string; mono: Mono } | null = null;
 
 	private _combo = 0;
 	private get combo() {
@@ -84,6 +87,7 @@ export class DropAndFusionGame extends EventEmitter<{
 		width: number;
 		height: number;
 		monoDefinitions: Mono[];
+		sfxVolume?: number;
 	}) {
 		super();
 
@@ -91,10 +95,14 @@ export class DropAndFusionGame extends EventEmitter<{
 		this.gameHeight = opts.height;
 		this.monoDefinitions = opts.monoDefinitions;
 
+		if (opts.sfxVolume) {
+			this.sfxVolume = opts.sfxVolume;
+		}
+
 		this.engine = Matter.Engine.create({
-			constraintIterations: 2 * PHYSICS_QUALITY_FACTOR,
-			positionIterations: 6 * PHYSICS_QUALITY_FACTOR,
-			velocityIterations: 4 * PHYSICS_QUALITY_FACTOR,
+			constraintIterations: 2 * this.PHYSICS_QUALITY_FACTOR,
+			positionIterations: 6 * this.PHYSICS_QUALITY_FACTOR,
+			velocityIterations: 4 * this.PHYSICS_QUALITY_FACTOR,
 			gravity: {
 				x: 0,
 				y: 1,
@@ -183,6 +191,7 @@ export class DropAndFusionGame extends EventEmitter<{
 		};
 		if (mono.shape === 'circle') {
 			return Matter.Bodies.circle(x, y, mono.size / 2, options);
+		// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
 		} else if (mono.shape === 'rectangle') {
 			return Matter.Bodies.rectangle(x, y, mono.size, mono.size, options);
 		} else {
@@ -224,7 +233,11 @@ export class DropAndFusionGame extends EventEmitter<{
 
 			// TODO: 効果音再生はコンポーネント側の責務なので移動する
 			const pan = ((newX / this.gameWidth) - 0.5) * 2;
-			sound.playUrl('/client-assets/drop-and-fusion/bubble2.mp3', 1, pan, nextMono.sfxPitch);
+			sound.playUrl('/client-assets/drop-and-fusion/bubble2.mp3', {
+				volume: this.sfxVolume,
+				pan,
+				playbackRate: nextMono.sfxPitch,
+			});
 
 			this.emit('monoAdded', nextMono);
 			this.emit('fusioned', newX, newY, additionalScore);
@@ -237,7 +250,7 @@ export class DropAndFusionGame extends EventEmitter<{
 			//}
 			//sound.playUrl({
 			//	type: 'syuilo/bubble2',
-			//	volume: 1,
+			//	volume: this.sfxVolume,
 			//});
 		}
 	}
@@ -323,10 +336,14 @@ export class DropAndFusionGame extends EventEmitter<{
 					const energy = pairs.collision.depth;
 					if (energy > minCollisionEnergyForSound) {
 						// TODO: 効果音再生はコンポーネント側の責務なので移動する
-						const vol = (Math.min(maxCollisionEnergyForSound, energy - minCollisionEnergyForSound) / maxCollisionEnergyForSound) / 4;
+						const vol = ((Math.min(maxCollisionEnergyForSound, energy - minCollisionEnergyForSound) / maxCollisionEnergyForSound) / 4) * this.sfxVolume;
 						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.playUrl('/client-assets/drop-and-fusion/poi1.mp3', vol, pan, pitch);
+						sound.playUrl('/client-assets/drop-and-fusion/poi1.mp3', {
+							volume: vol,
+							pan,
+							playbackRate: pitch,
+						});
 					}
 				}
 			}
@@ -344,6 +361,10 @@ export class DropAndFusionGame extends EventEmitter<{
 		this.loaded = true;
 	}
 
+	public setSfxVolume(volume: number) {
+		this.sfxVolume = volume;
+	}
+
 	public getTextureImageUrl(mono: Mono) {
 		// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
 		if (this.monoTextureUrls[mono.img]) {
@@ -366,28 +387,55 @@ export class DropAndFusionGame extends EventEmitter<{
 
 	public drop(_x: number) {
 		if (this.isGameOver) return;
-		if (Date.now() - this.latestDroppedAt < this.DROP_INTERVAL) {
-			return;
-		}
-		const st = this.stock.shift()!;
+		if (Date.now() - this.latestDroppedAt < this.DROP_INTERVAL) return;
+
+		const head = this.stock.shift()!;
 		this.stock.push({
 			id: Math.random().toString(),
 			mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(Math.random() * this.monoDefinitions.filter(x => x.dropCandidate).length)],
 		});
 		this.emit('changeStock', this.stock);
 
-		const x = Math.min(this.gameWidth - this.PLAYAREA_MARGIN - (st.mono.size / 2), Math.max(this.PLAYAREA_MARGIN + (st.mono.size / 2), _x));
-		const body = this.createBody(st.mono, x, 50 + st.mono.size / 2);
+		const x = Math.min(this.gameWidth - this.PLAYAREA_MARGIN - (head.mono.size / 2), Math.max(this.PLAYAREA_MARGIN + (head.mono.size / 2), _x));
+		const body = this.createBody(head.mono, x, 50 + head.mono.size / 2);
 		Matter.Composite.add(this.engine.world, body);
 		this.activeBodyIds.push(body.id);
 		this.latestDroppedBodyId = body.id;
 		this.latestDroppedAt = Date.now();
 		this.emit('dropped');
-		this.emit('monoAdded', st.mono);
+		this.emit('monoAdded', head.mono);
 
 		// TODO: 効果音再生はコンポーネント側の責務なので移動する
 		const pan = ((x / this.gameWidth) - 0.5) * 2;
-		sound.playUrl('/client-assets/drop-and-fusion/poi2.mp3', 1, pan);
+		sound.playUrl('/client-assets/drop-and-fusion/poi2.mp3', {
+			volume: this.sfxVolume,
+			pan,
+		});
+	}
+
+	public hold() {
+		if (this.isGameOver) return;
+
+		if (this.holding) {
+			const head = this.stock.shift()!;
+			this.stock.unshift(this.holding);
+			this.holding = head;
+			this.emit('changeHolding', this.holding);
+			this.emit('changeStock', this.stock);
+		} else {
+			const head = this.stock.shift()!;
+			this.holding = head;
+			this.stock.push({
+				id: Math.random().toString(),
+				mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(Math.random() * this.monoDefinitions.filter(x => x.dropCandidate).length)],
+			});
+			this.emit('changeHolding', this.holding);
+			this.emit('changeStock', this.stock);
+		}
+
+		sound.playUrl('/client-assets/drop-and-fusion/hold.mp3', {
+			volume: 0.5 * this.sfxVolume,
+		});
 	}
 
 	public dispose() {
diff --git a/packages/frontend/src/scripts/sound.ts b/packages/frontend/src/scripts/sound.ts
index 690c342c85..142ddf87c9 100644
--- a/packages/frontend/src/scripts/sound.ts
+++ b/packages/frontend/src/scripts/sound.ts
@@ -126,13 +126,13 @@ export async function loadAudio(url: string, options?: { useCache?: boolean; })
  * 既定のスプライトを再生する
  * @param type スプライトの種類を指定
  */
-export function play(operationType: OperationType) {
+export function playMisskeySfx(operationType: OperationType) {
 	const sound = defaultStore.state[`sound_${operationType}`];
 	if (_DEV_) console.log('play', operationType, sound);
 	if (sound.type == null || !canPlay) return;
 
 	canPlay = false;
-	playFile(sound).finally(() => {
+	playMisskeySfxFile(sound).finally(() => {
 		// ごく短時間に音が重複しないように
 		setTimeout(() => {
 			canPlay = true;
@@ -144,41 +144,53 @@ export function play(operationType: OperationType) {
  * サウンド設定形式で指定された音声を再生する
  * @param soundStore サウンド設定
  */
-export async function playFile(soundStore: SoundStore) {
+export async function playMisskeySfxFile(soundStore: SoundStore) {
 	if (soundStore.type === null || (soundStore.type === '_driveFile_' && !soundStore.fileUrl)) {
 		return;
 	}
+	const masterVolume = defaultStore.state.sound_masterVolume;
+	if (isMute() || masterVolume === 0 || soundStore.volume === 0) {
+		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)?.soundSource.start();
+	const volume = soundStore.volume * masterVolume;
+	createSourceNode(buffer, { volume }).soundSource.start();
 }
 
-export async function playUrl(url: string, volume = 1, pan = 0, playbackRate = 1) {
+export async function playUrl(url: string, opts: {
+	volume?: number;
+	pan?: number;
+	playbackRate?: number;
+}) {
+	if (opts.volume === 0) {
+		return;
+	}
 	const buffer = await loadAudio(url);
 	if (!buffer) return;
-	createSourceNode(buffer, volume, pan, playbackRate)?.soundSource.start();
+	createSourceNode(buffer, opts).soundSource.start();
 }
 
-export function createSourceNode(buffer: AudioBuffer, volume: number, pan = 0, playbackRate = 1): {
+export function createSourceNode(buffer: AudioBuffer, opts: {
+	volume?: number;
+	pan?: number;
+	playbackRate?: number;
+}): {
 	soundSource: AudioBufferSourceNode;
 	panNode: StereoPannerNode;
 	gainNode: GainNode;
-} | null {
-	const masterVolume = defaultStore.state.sound_masterVolume;
-	if (isMute() || masterVolume === 0 || volume === 0) {
-		return null;
-	}
-
+} {
 	const panNode = ctx.createStereoPanner();
-	panNode.pan.value = pan;
+	panNode.pan.value = opts.pan ?? 0;
 
 	const gainNode = ctx.createGain();
-	gainNode.gain.value = masterVolume * volume;
+
+	gainNode.gain.value = opts.volume ?? 1;
 
 	const soundSource = ctx.createBufferSource();
 	soundSource.buffer = buffer;
-	soundSource.playbackRate.value = playbackRate;
+	soundSource.playbackRate.value = opts.playbackRate ?? 1;
 	soundSource
 		.connect(panNode)
 		.connect(gainNode)
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index c60dfb9388..ea7c248003 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -468,6 +468,13 @@ export const defaultStore = markRaw(new Storage('base', {
 		where: 'device',
 		default: false,
 	},
+	dropAndFusion: {
+		where: 'device',
+		default: {
+			bgmVolume: 0.25,
+			sfxVolume: 1,
+		},
+	},
 
 	sound_masterVolume: {
 		where: 'device',
diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue
index 78af49cdc2..0ec036c5cb 100644
--- a/packages/frontend/src/ui/_common_/common.vue
+++ b/packages/frontend/src/ui/_common_/common.vue
@@ -83,7 +83,7 @@ function onNotification(notification: Misskey.entities.Notification, isClient =
 		}, 6000);
 	}
 
-	sound.play('notification');
+	sound.playMisskeySfx('notification');
 }
 
 if ($i) {
diff --git a/packages/frontend/src/widgets/WidgetJobQueue.vue b/packages/frontend/src/widgets/WidgetJobQueue.vue
index e5d8a3e5ea..01a79e7d77 100644
--- a/packages/frontend/src/widgets/WidgetJobQueue.vue
+++ b/packages/frontend/src/widgets/WidgetJobQueue.vue
@@ -123,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)?.soundSource;
+			const soundNode = sound.createSourceNode(jammedAudioBuffer.value, {}).soundSource;
 			if (soundNode) {
 				jammedSoundNodePlaying.value = true;
 				soundNode.onended = () => jammedSoundNodePlaying.value = false;