Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 2026-05-07 - AudioContext Singleton Pattern
**Learning:** In static tool applications like `go.html` and `getagoal.html`, creating a new `AudioContext` on every user interaction (e.g., button click for sound feedback) is extremely inefficient and can quickly hit browser limits on the number of active contexts. Instantiation is heavy (~5.6ms per context in benchmarks), while reuse is nearly instantaneous (<0.01ms).
**Action:** Always implement a singleton pattern for `AudioContext` in tools requiring sound effects. Initialize the context lazily within a user gesture handler to comply with autoplay policies, and store it in a global or persistent variable for reuse.
17 changes: 11 additions & 6 deletions getagoal.html
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@ <h2 class="text-xl font-bold mb-4">編輯計數器</h2>
<script>
const { createApp, ref, computed, onMounted, watch } = Vue;

// Performance optimization: Singleton for AudioContext to avoid high overhead and browser limits
let audioCtx = null;

// --- Google API 設定 ---
const CLIENT_ID = '794911308416-v7pbkb5etod2pr7is62e8ugq32dnj9ia.apps.googleusercontent.com';
const DISCOVERY_DOC = 'https://www.googleapis.com/discovery/v1/apis/drive/v3/rest';
Expand Down Expand Up @@ -312,13 +315,15 @@ <h2 class="text-xl font-bold mb-4">編輯計數器</h2>

const handleSoundFeedback = (item) => {
if (item.sound === 'tick') {
const ctx = new (window.AudioContext || window.webkitAudioContext)();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain); gain.connect(ctx.destination);
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain); gain.connect(audioCtx.destination);
osc.frequency.value = 850;
gain.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + 0.1);
osc.start(); osc.stop(ctx.currentTime + 0.1);
gain.gain.exponentialRampToValueAtTime(0.0001, audioCtx.currentTime + 0.1);
osc.start(); osc.stop(audioCtx.currentTime + 0.1);
} else if (item.sound === 'voice') {
const msg = new SpeechSynthesisUtterance(item.current_value.toString());
msg.lang = 'zh-TW';
Expand Down
17 changes: 11 additions & 6 deletions go.html
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ <h2 class="text-xl font-bold mb-4">編輯計數器</h2>
<script>
const { createApp, ref, computed, onMounted, watch } = Vue;

// Performance optimization: Singleton for AudioContext to avoid high overhead and browser limits
let audioCtx = null;

createApp({
setup() {
const counters = ref([]);
Expand Down Expand Up @@ -152,15 +155,17 @@ <h2 class="text-xl font-bold mb-4">編輯計數器</h2>

const handleSound = (item) => {
if (item.sound === 'tick') {
const ctx = new (window.AudioContext || window.webkitAudioContext)();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(ctx.destination);
gain.connect(audioCtx.destination);
osc.frequency.value = 800;
gain.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + 0.1);
gain.gain.exponentialRampToValueAtTime(0.0001, audioCtx.currentTime + 0.1);
osc.start();
osc.stop(ctx.currentTime + 0.1);
osc.stop(audioCtx.currentTime + 0.1);
} else if (item.sound === 'voice') {
const msg = new SpeechSynthesisUtterance(item.current_value.toString());
msg.lang = 'zh-TW';
Expand Down