Skip to content

Night Shift: fix stale onEnded closure in audio hook#23

Open
EtanHey wants to merge 1 commit intomasterfrom
nightshift/2026-02-28-3821
Open

Night Shift: fix stale onEnded closure in audio hook#23
EtanHey wants to merge 1 commit intomasterfrom
nightshift/2026-02-28-3821

Conversation

@EtanHey
Copy link
Owner

@EtanHey EtanHey commented Feb 28, 2026

User description

Automated improvement by Golems Night Shift.

fix stale onEnded closure in audio hook


PR Type

Bug fix


Description

  • Fix stale onEnded closure by using ref pattern

  • Ensures latest callback fires without reloading audio

  • Apply consistent code formatting with semicolons


Diagram Walkthrough

flowchart LR
  A["onEnded callback parameter"] -->|"stored in ref"| B["onEndedRef"]
  B -->|"updated on each render"| C["onEndedRef.current"]
  C -->|"called by handler"| D["handleEnded function"]
  D -->|"no effect re-run needed"| E["Audio elements persist"]
Loading

File Walkthrough

Relevant files
Bug fix
useAudioPreloader.ts
Fix stale closure and apply code formatting                           

src/hooks/useAudioPreloader.ts

  • Introduce onEndedRef to store the onEnded callback and update it on
    each render
  • Replace direct onEnded call with onEndedRef.current?.() in handleEnded
    handler
  • Remove onEnded from effect dependencies to prevent unnecessary audio
    reloading
  • Apply consistent code formatting with double quotes and semicolons
    throughout
+113/-99

Summary by CodeRabbit

  • Refactor
    • Improved audio callback handling consistency in the preloader hook
    • Standardized code formatting and syntax across the module for better maintainability

The onEnded callback was captured in the useEffect closure but not
included in the dependency array. If the caller updated onEnded between
renders, the stale version from the initial render would fire instead.
Use a ref pattern so the ended handler always calls the latest callback
without needing to re-run the effect (which would reload all audio).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@vercel
Copy link

vercel bot commented Feb 28, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
songscript Building Building Preview, Comment Feb 28, 2026 2:10am

@coderabbitai
Copy link

coderabbitai bot commented Feb 28, 2026

📝 Walkthrough

Walkthrough

A single React hook file receives TypeScript syntax standardization (semicolons, trailing commas) alongside a new onEndedRef mechanism to preserve audio callback references without triggering unnecessary reloads. Interface definitions for audio state management are refined, and internal ref-based handling logic is introduced while maintaining existing behavioral contracts.

Changes

Cohort / File(s) Summary
Audio Preloader Hook Refinement
src/hooks/useAudioPreloader.ts
Added onEndedRef mechanism for callback persistence; standardized TypeScript syntax (semicolons, trailing commas); refined AudioSnippet, PreloadState, and UseAudioPreloaderReturn interfaces; updated useAudioPreloader function signature with onEnded callback parameter.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Poem

🐰 With refs held tight and syntax clean,
The hook now dances lean and lean,
No reloads on each fleeting render—
Callbacks persist, oh what a splendor!
TypeScript smiles at comma care,
Audio flows with grace and flair. ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly addresses the main change: fixing a stale onEnded closure in the audio hook using a ref pattern, which aligns with the core objective and commit message.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch nightshift/2026-02-28-3821

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@qodo-code-review
Copy link

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
🟢
No security concerns identified No security vulnerabilities detected by AI analysis. Human verification advised for critical code.
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Incomplete event cleanup: The cleanup attempts to remove the ended listener using a new anonymous function, which
will not unregister the originally attached handler and can cause lingering
listeners/memory leaks across snippet changes.

Referred Code
return () => {
  // Cleanup all audio elements on unmount
  audioMapRef.current.forEach((audio) => {
    audio.pause();
    audio.removeEventListener("ended", () => {});
    audio.src = "";
  });

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
URL logged in warn: The console.warn logs include audioUrl, which may contain sensitive query parameters
(e.g., signed URLs/tokens) and should be reviewed/redacted to avoid leaking secrets in
logs.

Referred Code
const handleError = () => {
  if (handled) return;
  handled = true;
  // Still increment loaded count to not block ready state
  loadedCount++;
  console.warn(
    `Failed to load audio for line ${lineNumber}: ${audioUrl}`,
  );

Learn more about managing compliance generic rules or creating your own custom rules

Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review
Copy link

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Fix memory leak from unremoved listeners

Fix a memory leak by correctly removing the 'ended' event listener in the
cleanup function. Store the handleEnded function reference for each audio
element and use it in removeEventListener.

src/hooks/useAudioPreloader.ts [99-113]

-  audio.addEventListener("ended", handleEnded);
-  audio.src = audioUrl;
-  audio.load(); // Explicitly trigger loading
-});
+  const listenersRef = useRef<Map<HTMLAudioElement, () => void>>(new Map());
+  listenersRef.current.clear(); // Clear listeners from previous effect run
 
-return () => {
-  // Cleanup all audio elements on unmount
-  audioMapRef.current.forEach((audio) => {
-    audio.pause();
-    audio.removeEventListener("ended", () => {});
-    audio.src = "";
+  snippets.forEach(({ lineNumber, audioUrl }) => {
+    if (!audioUrl) return;
+
+    const audio = new Audio();
+    audio.preload = "auto";
+    let handled = false;
+
+    const handleCanPlay = () => {
+      if (handled) return;
+      handled = true;
+      loadedCount++;
+      audioMapRef.current.set(lineNumber, audio);
+      setState({
+        loaded: loadedCount,
+        total,
+        ready: loadedCount === total,
+      });
+    };
+
+    const handleError = () => {
+      if (handled) return;
+      handled = true;
+      loadedCount++;
+      console.warn(
+        `Failed to load audio for line ${lineNumber}: ${audioUrl}`,
+      );
+      setState({
+        loaded: loadedCount,
+        total,
+        ready: loadedCount === total,
+      });
+    };
+
+    const handleEnded = () => {
+      setIsPlaying(false);
+      onEndedRef.current?.(lineNumber);
+    };
+
+    audio.addEventListener("canplaythrough", handleCanPlay, { once: true });
+    audio.addEventListener("canplay", handleCanPlay, { once: true });
+    audio.addEventListener("error", handleError, { once: true });
+    audio.addEventListener("ended", handleEnded);
+    listenersRef.current.set(audio, handleEnded); // Store the listener
+    audio.src = audioUrl;
+    audio.load();
   });
-  audioMapRef.current.clear();
-  currentlyPlayingRef.current = null;
-};
 
+  return () => {
+    audioMapRef.current.forEach((audio) => {
+      audio.pause();
+      const listener = listenersRef.current.get(audio);
+      if (listener) {
+        audio.removeEventListener("ended", listener);
+      }
+      audio.src = "";
+    });
+    audioMapRef.current.clear();
+    currentlyPlayingRef.current = null;
+  };
+

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a significant memory leak caused by failing to properly remove event listeners in the useEffect cleanup function and provides a robust, correct solution.

High
  • More

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/hooks/useAudioPreloader.ts`:
- Around line 116-135: The play function can leave stale state when the
requested audio is missing; after stopping any currentlyPlayingRef, ensure you
clear currentlyPlayingRef.current and call setIsPlaying(false) if
audioMapRef.current.get(lineNumber) is undefined or null before returning;
modify the play callback (function play) to check for the audio entry and
explicitly reset currentlyPlayingRef and isPlaying when no audio is found, while
keeping the existing behavior when audio exists (setting playbackRate, loop,
handling play promise and updating currentlyPlayingRef/setIsPlaying).
- Around line 50-59: The bug is that total is set to snippets.length but
snippets.forEach skips entries with empty audioUrl, so loadedCount never reaches
total and ready stays false; fix by either (a) compute total as the number of
snippets with a non-empty audioUrl before starting (e.g., derive total =
snippets.filter(s => s.audioUrl).length) or (b) when skipping inside
snippets.forEach (the if (!audioUrl) return), increment loadedCount and update
setState accordingly; ensure you update setState({ loaded, total, ready: loaded
=== total }) and reference loadedCount, total, setState, snippets.forEach,
audioUrl and audioMapRef.current.clear to apply the change.
- Around line 24-27: Add a new test file for the useAudioPreloader hook (e.g.,
src/hooks/__tests__/useAudioPreloader.test.tsx) that verifies regression
coverage for the onEnded callback behavior: mount a component that calls
useAudioPreloader with a small list of AudioSnippet items, spy/mock
HTMLAudioElement events or provide a fake audio element to avoid network, assert
that when you update the onEnded prop to a new function it is invoked on audio
end using the latest function instance (referencing onEnded and
useAudioPreloader) and that the hook does not re-run its effect or recreate
audio elements when the callback changes (i.e., audio elements remain the same
across rerenders). Use React Testing Library or similar to rerender, advance
timers or dispatch the 'ended' event to trigger callbacks, and assert no
additional audio element loads occurred; run and confirm bun run test passes
locally before committing.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 93c1185 and 78c26bc.

📒 Files selected for processing (1)
  • src/hooks/useAudioPreloader.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use TanStack Start framework with Bun runtime for the application

Tests must pass locally via bun run test before committing code, as Husky pre-commit hooks will block commits with failing tests

Files:

  • src/hooks/useAudioPreloader.ts
**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

All new helpers and utilities MUST have corresponding test files

Files:

  • src/hooks/useAudioPreloader.ts
🔇 Additional comments (1)
src/hooks/useAudioPreloader.ts (1)

39-43: Good stale-closure fix for onEnded.

Using onEndedRef.current in the ended handler is the right approach here and avoids effect re-runs/reloads while still invoking the latest callback.

Also applies to: 91-94

Comment on lines 24 to 27
export function useAudioPreloader(
snippets: AudioSnippet[],
onEnded?: (lineNumber: number) => void
onEnded?: (lineNumber: number) => void,
): UseAudioPreloaderReturn {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find test files for useAudioPreloader
fd -i 'useaudiopreloader' --type f

Repository: EtanHey/songscript

Length of output: 92


🏁 Script executed:

# Find any test files in the hooks directory
fd '(test|spec)\.(ts|tsx)$' src/hooks/ || fd 'hooks' src/ --type d

Repository: EtanHey/songscript

Length of output: 130


🏁 Script executed:

# Look at the hook implementation
cat -n src/hooks/useAudioPreloader.ts | head -50

Repository: EtanHey/songscript

Length of output: 1822


🏁 Script executed:

# Search for onEnded references in test files
rg -l 'onEnded' --type ts --type tsx src/

Repository: EtanHey/songscript

Length of output: 89


🏁 Script executed:

# Search more comprehensively for any test files related to useAudioPreloader
find . -type f -name "*useAudioPreloader*"

Repository: EtanHey/songscript

Length of output: 94


🏁 Script executed:

# Check if there are test files in a different location
find src/ -type f \( -name "*.test.ts" -o -name "*.spec.ts" -o -name "*.test.tsx" -o -name "*.spec.tsx" \)

Repository: EtanHey/songscript

Length of output: 435


🏁 Script executed:

# Search for onEnded in the full hook implementation to understand the fix better
cat -n src/hooks/useAudioPreloader.ts

Repository: EtanHey/songscript

Length of output: 6642


🏁 Script executed:

# Check if there's a test directory at root level
find . -maxdepth 2 -type d -name "*test*" -o -name "*__tests__*"

Repository: EtanHey/songscript

Length of output: 88


Add test file for useAudioPreloader hook with regression coverage for the onEnded callback behavior.

The hook is missing a test file entirely, violating the coding guideline requiring test files for all utilities. Additionally, add a focused regression test confirming that the onEnded callback (lines 39–42) correctly uses the latest callback instance when it changes between renders, without triggering effect re-runs that would reload audio elements. Ensure bun run test passes locally before committing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useAudioPreloader.ts` around lines 24 - 27, Add a new test file for
the useAudioPreloader hook (e.g.,
src/hooks/__tests__/useAudioPreloader.test.tsx) that verifies regression
coverage for the onEnded callback behavior: mount a component that calls
useAudioPreloader with a small list of AudioSnippet items, spy/mock
HTMLAudioElement events or provide a fake audio element to avoid network, assert
that when you update the onEnded prop to a new function it is invoked on audio
end using the latest function instance (referencing onEnded and
useAudioPreloader) and that the hook does not re-run its effect or recreate
audio elements when the callback changes (i.e., audio elements remain the same
across rerenders). Use React Testing Library or similar to rerender, advance
timers or dispatch the 'ended' event to trigger callbacks, and assert no
additional audio element loads occurred; run and confirm bun run test passes
locally before committing.

Comment on lines +50 to 59
let loadedCount = 0;
const total = snippets.length;

// Reset state for new snippets
setState({ loaded: 0, total, ready: false })
audioMapRef.current.clear()
setState({ loaded: 0, total, ready: false });
audioMapRef.current.clear();

snippets.forEach(({ lineNumber, audioUrl }) => {
if (!audioUrl) return
if (!audioUrl) return;

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

ready can stay false indefinitely when some snippets have empty audioUrl.

total is based on all snippets, but Line 58 skips empty URLs without incrementing loadedCount. In that case, loaded !== total forever.

Proposed fix
-    let loadedCount = 0;
-    const total = snippets.length;
+    let loadedCount = 0;
+    const loadableSnippets = snippets.filter(
+      ({ audioUrl }) => Boolean(audioUrl),
+    );
+    const total = loadableSnippets.length;

     // Reset state for new snippets
     setState({ loaded: 0, total, ready: false });
     audioMapRef.current.clear();

-    snippets.forEach(({ lineNumber, audioUrl }) => {
-      if (!audioUrl) return;
+    if (total === 0) {
+      setState({ loaded: 0, total: 0, ready: true });
+      return;
+    }
+
+    loadableSnippets.forEach(({ lineNumber, audioUrl }) => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let loadedCount = 0;
const total = snippets.length;
// Reset state for new snippets
setState({ loaded: 0, total, ready: false })
audioMapRef.current.clear()
setState({ loaded: 0, total, ready: false });
audioMapRef.current.clear();
snippets.forEach(({ lineNumber, audioUrl }) => {
if (!audioUrl) return
if (!audioUrl) return;
let loadedCount = 0;
const loadableSnippets = snippets.filter(
({ audioUrl }) => Boolean(audioUrl),
);
const total = loadableSnippets.length;
// Reset state for new snippets
setState({ loaded: 0, total, ready: false });
audioMapRef.current.clear();
if (total === 0) {
setState({ loaded: 0, total: 0, ready: true });
return;
}
loadableSnippets.forEach(({ lineNumber, audioUrl }) => {
if (!audioUrl) return;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useAudioPreloader.ts` around lines 50 - 59, The bug is that total
is set to snippets.length but snippets.forEach skips entries with empty
audioUrl, so loadedCount never reaches total and ready stays false; fix by
either (a) compute total as the number of snippets with a non-empty audioUrl
before starting (e.g., derive total = snippets.filter(s => s.audioUrl).length)
or (b) when skipping inside snippets.forEach (the if (!audioUrl) return),
increment loadedCount and update setState accordingly; ensure you update
setState({ loaded, total, ready: loaded === total }) and reference loadedCount,
total, setState, snippets.forEach, audioUrl and audioMapRef.current.clear to
apply the change.

Comment on lines 116 to +135
const play = useCallback((lineNumber: number) => {
// Stop any currently playing audio
if (currentlyPlayingRef.current) {
currentlyPlayingRef.current.pause()
currentlyPlayingRef.current.currentTime = 0
currentlyPlayingRef.current.pause();
currentlyPlayingRef.current.currentTime = 0;
}

const audio = audioMapRef.current.get(lineNumber)
const audio = audioMapRef.current.get(lineNumber);
if (audio) {
audio.currentTime = 0
audio.playbackRate = playbackRateRef.current
audio.loop = loopRef.current
audio.currentTime = 0;
audio.playbackRate = playbackRateRef.current;
audio.loop = loopRef.current;
audio.play().catch((err) => {
console.warn(`Failed to play audio for line ${lineNumber}:`, err)
setIsPlaying(false)
})
currentlyPlayingRef.current = audio
setIsPlaying(true)
console.warn(`Failed to play audio for line ${lineNumber}:`, err);
setIsPlaying(false);
});
currentlyPlayingRef.current = audio;
setIsPlaying(true);
}
}, [])
}, []);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

play() can leave stale playback state when target audio is unavailable.

After stopping current audio, if audioMapRef has no entry for the requested line, currentlyPlayingRef and isPlaying are not reset consistently.

Proposed fix
   const play = useCallback((lineNumber: number) => {
     // Stop any currently playing audio
     if (currentlyPlayingRef.current) {
       currentlyPlayingRef.current.pause();
       currentlyPlayingRef.current.currentTime = 0;
     }

     const audio = audioMapRef.current.get(lineNumber);
-    if (audio) {
-      audio.currentTime = 0;
-      audio.playbackRate = playbackRateRef.current;
-      audio.loop = loopRef.current;
-      audio.play().catch((err) => {
-        console.warn(`Failed to play audio for line ${lineNumber}:`, err);
-        setIsPlaying(false);
-      });
-      currentlyPlayingRef.current = audio;
-      setIsPlaying(true);
-    }
+    if (!audio) {
+      currentlyPlayingRef.current = null;
+      setIsPlaying(false);
+      return;
+    }
+
+    audio.currentTime = 0;
+    audio.playbackRate = playbackRateRef.current;
+    audio.loop = loopRef.current;
+    currentlyPlayingRef.current = audio;
+    setIsPlaying(true);
+    audio.play().catch((err) => {
+      console.warn(`Failed to play audio for line ${lineNumber}:`, err);
+      currentlyPlayingRef.current = null;
+      setIsPlaying(false);
+    });
   }, []);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const play = useCallback((lineNumber: number) => {
// Stop any currently playing audio
if (currentlyPlayingRef.current) {
currentlyPlayingRef.current.pause()
currentlyPlayingRef.current.currentTime = 0
currentlyPlayingRef.current.pause();
currentlyPlayingRef.current.currentTime = 0;
}
const audio = audioMapRef.current.get(lineNumber)
const audio = audioMapRef.current.get(lineNumber);
if (audio) {
audio.currentTime = 0
audio.playbackRate = playbackRateRef.current
audio.loop = loopRef.current
audio.currentTime = 0;
audio.playbackRate = playbackRateRef.current;
audio.loop = loopRef.current;
audio.play().catch((err) => {
console.warn(`Failed to play audio for line ${lineNumber}:`, err)
setIsPlaying(false)
})
currentlyPlayingRef.current = audio
setIsPlaying(true)
console.warn(`Failed to play audio for line ${lineNumber}:`, err);
setIsPlaying(false);
});
currentlyPlayingRef.current = audio;
setIsPlaying(true);
}
}, [])
}, []);
const play = useCallback((lineNumber: number) => {
// Stop any currently playing audio
if (currentlyPlayingRef.current) {
currentlyPlayingRef.current.pause();
currentlyPlayingRef.current.currentTime = 0;
}
const audio = audioMapRef.current.get(lineNumber);
if (!audio) {
currentlyPlayingRef.current = null;
setIsPlaying(false);
return;
}
audio.currentTime = 0;
audio.playbackRate = playbackRateRef.current;
audio.loop = loopRef.current;
currentlyPlayingRef.current = audio;
setIsPlaying(true);
audio.play().catch((err) => {
console.warn(`Failed to play audio for line ${lineNumber}:`, err);
currentlyPlayingRef.current = null;
setIsPlaying(false);
});
}, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useAudioPreloader.ts` around lines 116 - 135, The play function can
leave stale state when the requested audio is missing; after stopping any
currentlyPlayingRef, ensure you clear currentlyPlayingRef.current and call
setIsPlaying(false) if audioMapRef.current.get(lineNumber) is undefined or null
before returning; modify the play callback (function play) to check for the
audio entry and explicitly reset currentlyPlayingRef and isPlaying when no audio
is found, while keeping the existing behavior when audio exists (setting
playbackRate, loop, handling play promise and updating
currentlyPlayingRef/setIsPlaying).

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.

1 participant