Skip to content

Feat/frontend/play bgm#245

Open
arkwnet wants to merge 4 commits intodevelopfrom
feat/frontend/play-bgm
Open

Feat/frontend/play bgm#245
arkwnet wants to merge 4 commits intodevelopfrom
feat/frontend/play-bgm

Conversation

@arkwnet
Copy link
Copy Markdown
Contributor

@arkwnet arkwnet commented Apr 6, 2026

チケットへのリンク

close #69

やったこと

  • Web Audio APIによるサウンド再生機能の追加
    • 音声周りの処理はWebAudioPlayerコンポーネントで一元管理しています

やらないこと

容量が大きいBGMデータ自体のアップロード

できるようになること(ユーザ目線)

画面右下のボタンでBGMのON/OFFを切り替えられるようになります。暴発を防ぐため初期値はOFF、かつ再読み込みでリセットされます。

できなくなること(ユーザ目線)

無し

動作確認

全機能を目視で確認していますが、レビューされる方はお手元の環境でダブルチェックをお願いします (特に生協指定PCをお持ちの方)

その他

無し


Open with Devin

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 7 new potential issues.

Open in Devin Review

Comment on lines +10 to 12
});

return <RankingTabs />;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Missing [] dependency array causes play() to re-fire on every render, restarting BGM

In Ranking.tsx, useEffect(() => { play("/sounds/bgm0.mp3"); }) has no dependency array, so it runs after every render, not just on mount. Each call to play (in WebAudioPlayer.tsx:55) first calls stop() to halt the current audio, then restarts the track from the beginning. Since play and stop in WebAudioPlayer.tsx:80 are not memoized with useCallback, every render of WebAudioPlayer produces a new context value, triggering consumer re-renders — which in turn re-fire this effect. Concretely, toggling the sound button causes the ranking BGM to restart from the beginning, and any parent re-render will do the same.

Suggested change
});
return <RankingTabs />;
useEffect(() => {
play("/sounds/bgm0.mp3");
}, []);
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +10 to 12
});

return (
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Missing [] dependency array causes stop() to fire on every render

In Home.tsx, useEffect(() => { stop(); }) has no dependency array, so it runs after every render rather than only on mount. While stop() is idempotent when nothing is playing (it checks sourceRef.current), this still means that any re-render caused by context value changes or parent re-renders will unnecessarily invoke stop(). This is inconsistent with the analogous effects in GameResult.tsx:21-23 and GameTyping.tsx:197-211 which correctly use [].

Suggested change
});
return (
useEffect(() => {
stop();
}, []);
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

};
};

return <PlayerContext.Provider value={{ play, stop }}>{children}</PlayerContext.Provider>;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Context value not memoized — causes unnecessary consumer re-renders

In WebAudioPlayer.tsx:80, the value={{ play, stop }} creates a new object on every render since neither play nor stop are wrapped in useCallback. This means every re-render of WebAudioPlayer (e.g., when isPlay changes) creates a new context value, forcing all useContext(PlayerContext) consumers to re-render. This amplifies the impact of the missing dependency arrays in Ranking.tsx and Home.tsx (reported as bugs), and is generally wasteful. Wrapping play and stop in useCallback (and using an isPlayRef for the isPlay check inside play) would stabilize the context value and prevent unnecessary consumer re-renders.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +11 to +15
<>
<Footer isPlay={isPlay} setIsPlay={setIsPlay} />
<div className="children">
<WebAudioPlayer isPlay={isPlay}>{children}</WebAudioPlayer>
</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Footer rendered before children — visual ordering relies on CSS fixed positioning

In client.tsx:11-15, the Footer is rendered before the children div in the DOM. The previous layout in layout.tsx also had Footer before children, so this matches the existing pattern. The footer uses position: fixed (Footer.module.scss:6) so DOM order doesn't affect visual layout. Not a bug, just noting the ordering is intentional and CSS-dependent.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines 10 to +11
useEffect(() => {
play("/sounds/bgm1.mp3");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Stale play closure in game components won't respond to mid-page sound toggle

In GamePre.tsx:11, GameTyping.tsx:199-203, and GameResult.tsx:22, the play function is called inside useEffect hooks with [] or [nextPage] dependency arrays, but play itself is not included as a dependency. Since play captures isPlay via closure (WebAudioPlayer.tsx:53), the version captured at mount time won't reflect later toggles. This means if a user toggles sound ON while already on one of these pages, the BGM won't start (the captured play still has isPlay=false). This is a minor UX limitation rather than a crash-level bug — sound works correctly when navigating to a new page after toggling.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +60 to +64
if (!buffer) {
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();
buffer = await ctx.decodeAudioData(arrayBuffer);
bufferCacheRef.current.set(url, buffer);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: No error handling in play for failed fetch or decode

In WebAudioPlayer.tsx:61-63, fetch(url) and ctx.decodeAudioData(arrayBuffer) are both async operations that can throw (e.g., 404 response, invalid audio data), but neither is wrapped in a try/catch. An unhandled promise rejection would bubble up silently. Since play is called from useEffect callbacks which don't handle the returned promise, any rejection becomes an unhandled promise rejection warning. Consider wrapping the async body in a try/catch to prevent this.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

if (isProcessingRef.current) {
return;
}
stop();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: sendResultData uses stop without it in the dependency array

In GameTyping.tsx:57, stop() is called inside sendResultData which is wrapped in useCallback with deps [stats, nextPage, router, setScore] at line 108. stop from useWebAudio() is not included. I verified this is safe because stop (WebAudioPlayer.tsx:33-41) only accesses sourceRef.current, which is a ref and always points to the latest value regardless of closure staleness. So even a stale stop reference correctly stops the current audio source.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE]サウンドのトリガーボタンを作成

1 participant