Skip to content

Add transition extraction and Crossfade#8

Open
maiaryan wants to merge 5 commits into
mainfrom
feature/transition-extraction
Open

Add transition extraction and Crossfade#8
maiaryan wants to merge 5 commits into
mainfrom
feature/transition-extraction

Conversation

@maiaryan

@maiaryan maiaryan commented Sep 27, 2025

Copy link
Copy Markdown
Collaborator

Summary by Sourcery

Add a Python module to extract and crossfade transition clips from DJ mixes by slicing adjacent track segments and stitching them into a final MP3 file

New Features:

  • Implement extract_segments to cut previous track tail, mix snippet around boundary, and next track head using ffmpeg
  • Add crossfade and stitch_to_final functions with safe duration checks to merge audio segments and encode as MP3
  • Include a demo main() entrypoint illustrating the end-to-end extraction and crossfade process

Summary by CodeRabbit

  • New Features
    • Local audio transition tool to extract segments and crossfade across three tracks.
    • Generates tail, middle and head clips and stitches them into a final MP3.
    • Automatically limits crossfade length to prevent artifacts and logs when shortened.
    • Saves outputs to a transitions output folder and includes a demo script to preview results.

@sourcery-ai

sourcery-ai Bot commented Sep 27, 2025

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Introduces a new transition extraction module that slices audio around transition boundaries, applies configurable crossfades, and outputs a final MP3 clip using ffmpeg utilities.

File-Level Changes

Change Details Files
Set up subprocess runner and output directory
  • Added run() wrapper for cross-platform ffmpeg/ffprobe calls
  • Defined OUT_DIR and ensured it exists before writing files
transitions/transition_extraction.py
Implemented audio duration probing with error handling
  • Created duration() using ffprobe to fetch clip length
  • Added exception handling for invalid ffprobe output
transitions/transition_extraction.py
Built segment extraction logic around detected boundary
  • Validated input parameters against negative and out‐of‐range values
  • Computed start times for prev tail, mix segment, and next head
  • Used ffmpeg to cut consistent WAV snippets for each segment
transitions/transition_extraction.py
Added safe crossfade duration computation
  • Computed maximum allowable crossfade from clip lengths
  • Fell back to 80% of shortest clip if desired crossfade is too long
transitions/transition_extraction.py
Created crossfade and final stitching routines
  • Implemented crossfade() to merge two WAVs with ffmpeg acrossfade filter
  • Built stitch_to_final() to crossfade and encode output as MP3
transitions/transition_extraction.py
Provided main demo pipeline
  • Wired extract_segments → crossfade → stitch_to_final in main()
  • Set up hardcoded test paths and boundary for manual verification
transitions/transition_extraction.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Sep 27, 2025

Copy link
Copy Markdown

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds a new module transitions/transition_extraction.py implementing audio duration probing, segment extraction around a boundary, safe crossfade computation, WAV crossfading and MP3 stitching workflows (outputs to out/transitions), plus a new example runner transitions/run_local_test.py demonstrating end-to-end usage.

Changes

Cohort / File(s) Summary of changes
Audio transition extraction module
transitions/transition_extraction.py
New module that: creates out/transitions; provides run(*args) subprocess helper with logging; duration(path: str) -> float (ffprobe); extract_segments(prev_path, mix_path, next_path, boundary_s, pre_tail=1.0, mix_before=0.5, mix_after=0.5, post_head=1.0) producing prev_tail.wav, mix_mid.wav, next_head.wav; safe_crossfade_duration(a_path, b_path, desired_cross) clamps crossfade to ≤80% of shorter clip; crossfade(a_path, b_path, out_path, cross=0.3) using ffmpeg acrossfade; stitch_to_final(step1, next_head, mp3_out, cross=0.3) producing MP3 via libmp3lame 192k; validates inputs and logs warnings/errors.
Local test runner script
transitions/run_local_test.py
New CLI/script with main() that imports functions from transition_extraction, sets a boundary, calls extract_segments, creates a crossfaded prev_plus_mix.wav, and writes final transition_demo.mp3 in out/transitions.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Runner as run_local_test.py
  participant TE as transition_extraction.py
  participant FFPROBE as ffprobe
  participant FFMPEG as ffmpeg
  participant FS as FileSystem

  User->>Runner: main()
  Runner->>TE: extract_segments(prev,mix,next,boundary)
  TE->>FS: ensure out/transitions exists
  TE->>FFPROBE: probe durations
  FFPROBE-->>TE: durations

  rect rgba(200,230,255,0.18)
    note over TE: extract_segments cuts three clips
    TE->>FFMPEG: cut prev_tail.wav, mix_mid.wav, next_head.wav
    FFMPEG-->>FS: write WAV clips
  end

  rect rgba(220,255,220,0.12)
    note over TE: first crossfade
    TE->>TE: safe_crossfade_duration(...)
    TE->>FFMPEG: acrossfade -> step1.wav
    FFMPEG-->>FS: write step1.wav
  end

  rect rgba(255,240,200,0.12)
    note over TE: final stitch to MP3
    TE->>TE: safe_crossfade_duration(...)
    TE->>FFMPEG: acrossfade + encode MP3 (libmp3lame 192k)
    FFMPEG-->>FS: transition_demo.mp3
  end

  Runner-->>User: paths to generated files
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

I nibble tails and hum the seams,
I count with ffprobe, stitch with dreams,
Across the fades my whiskers play,
I hop the beats and save the day. 🐇🎶

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ 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 “Add transition extraction and Crossfade” succinctly highlights the addition of the transition extraction module and crossfade functionality, directly reflecting the primary features introduced by this pull request without extraneous detail.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/transition-extraction

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6af5391 and c6134b2.

📒 Files selected for processing (1)
  • transitions/transition_extraction.py (1 hunks)
🧰 Additional context used
🪛 Ruff (0.13.1)
transitions/transition_extraction.py

43-43: subprocess call: check for execution of untrusted input

(S603)


44-44: Unused noqa directive (unused: S603, S607)

Remove unused noqa directive

(RUF100)


52-52: Unused noqa directive (unused: S603, S607)

Remove unused noqa directive

(RUF100)


59-59: subprocess call: check for execution of untrusted input

(S603)


59-64: Starting a process with a partial executable path

(S607)


70-70: Avoid specifying long messages outside the exception class

(TRY003)


96-96: Avoid specifying long messages outside the exception class

(TRY003)


100-100: Avoid specifying long messages outside the exception class

(TRY003)


152-152: Comment contains ambiguous (RIGHT SINGLE QUOTATION MARK). Did you mean ``` (GRAVE ACCENT)?

(RUF003)


168-168: Avoid specifying long messages outside the exception class

(TRY003)


170-170: Avoid specifying long messages outside the exception class

(TRY003)

🪛 Flake8 (7.2.0)
transitions/transition_extraction.py

[error] 3-3: block comment should start with '# '

(E265)


[error] 6-6: block comment should start with '# '

(E265)


[error] 10-10: block comment should start with '# '

(E265)


[error] 11-11: block comment should start with '# '

(E265)


[error] 12-12: block comment should start with '# '

(E265)


[error] 18-18: block comment should start with '# '

(E265)


[error] 26-26: block comment should start with '# '

(E265)


[error] 83-83: block comment should start with '# '

(E265)


[error] 84-84: block comment should start with '# '

(E265)


[error] 85-85: block comment should start with '# '

(E265)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Sourcery review
🔇 Additional comments (1)
transitions/transition_extraction.py (1)

145-157: Guard against zero/negative crossfade durations

safe_crossfade_duration currently returns zero (or even negative) if the caller passes a non‑positive cross, or if one of the clips ends up effectively empty (e.g., pre_tail=0). ffmpeg’s acrossfade rejects d<=0, so we’ll raise a CalledProcessError at runtime instead of surfacing a clear validation error. Please enforce positivity before spawning ffmpeg.

 def safe_crossfade_duration(a_path: Path, b_path: Path, desired_cross: float) -> float:
     len_a = duration(str(a_path))
     len_b = duration(str(b_path))

     # pick the shortest file’s length * 0.8 to stay safe
     max_possible = min(len_a, len_b) * 0.8
+    if desired_cross <= 0:
+        raise ValueError(f"Crossfade duration must be positive (got {desired_cross}).")
+    if max_possible <= 0:
+        raise ValueError(
+            f"Cannot crossfade clips with no usable audio ({len_a:.2f}s, {len_b:.2f}s)."
+        )
     if desired_cross > max_possible:
         logger.warning("Crossfade duration adjusted due to short clip")
         return max_possible
     return desired_cross

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

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes - here's some feedback:

Blocking issues:

  • Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)

General comments:

  • Extract the main() function and any hard-coded test paths into a separate script or entry point so this module only contains reusable logic.
  • Replace ad-hoc print statements for missing files and ffmpeg warnings with a consistent logging or exception strategy to let callers handle errors robustly.
  • Parameterize hard-coded paths like OUT_DIR and test file locations through function arguments or configuration to make the module more flexible across environments.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Extract the `main()` function and any hard-coded test paths into a separate script or entry point so this module only contains reusable logic.
- Replace ad-hoc print statements for missing files and ffmpeg warnings with a consistent logging or exception strategy to let callers handle errors robustly.
- Parameterize hard-coded paths like `OUT_DIR` and test file locations through function arguments or configuration to make the module more flexible across environments.

## Individual Comments

### Comment 1
<location> `transitions/transition_extraction.py:32-41` </location>
<code_context>
+    #print("→", " ".join(args))
+    subprocess.run(list(args), check=True)
+
+def duration(path: str) -> float:
+    """Return audio duration in seconds using ffprobe."""
+    out = subprocess.check_output([
+        "ffprobe", "-v", "error",
+        "-show_entries", "format=duration",
+        "-of", "default=noprint_wrappers=1:nokey=1",
+        path
+    ])
+
+    # Error Handling
+    try:
+        return float(out.strip())
+    except ValueError as e:
+        raise RuntimeError(f"Could not decode ffprobe output for {path}: {out!r}") from e
</code_context>

<issue_to_address>
**nitpick:** The final return statement after the exception block is unreachable.

The final 'return float(out.strip())' is redundant and should be removed.
</issue_to_address>

### Comment 2
<location> `transitions/transition_extraction.py:143-148` </location>
<code_context>
+    # Use helper to determine best crossfade
+    
+    # Check for existence of input files
+    if not a_path.exists():
+        print(f"Input file does not exist: {a_path}")
+        return
+    if not b_path.exists():
+        print(f"Input file does not exist: {b_path}")
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Returning early on missing input files may mask errors downstream.

Raising an exception for missing input files will ensure errors are properly surfaced and handled throughout the workflow.

```suggestion
    if not a_path.exists():
        raise FileNotFoundError(f"Input file does not exist: {a_path}")
    if not b_path.exists():
        raise FileNotFoundError(f"Input file does not exist: {b_path}")
```
</issue_to_address>

### Comment 3
<location> `transitions/transition_extraction.py:30` </location>
<code_context>
    subprocess.run(list(args), check=True)
</code_context>

<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

*Source: opengrep*
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread transitions/transition_extraction.py
Comment thread transitions/transition_extraction.py Outdated
Comment thread transitions/transition_extraction.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
transitions/transition_extraction.py (2)

77-80: Avoid clobbering segment outputs between runs

Line 77 hardcodes the filenames in out/transitions, so each call overwrites the previous run. That makes it unsafe to process multiple boundaries in one session or run jobs in parallel. Please bake in a unique prefix (e.g., derived from track name + boundary) or allow the caller to provide output paths so each invocation produces distinct artifacts.

-    p_tail = OUT_DIR / "prev_tail.wav"
-    m_mid  = OUT_DIR / "mix_mid.wav"
-    n_head = OUT_DIR / "next_head.wav"
+    prefix = f"{Path(mix_path).stem}_{boundary_s:.3f}"
+    p_tail = OUT_DIR / f"{prefix}_prev_tail.wav"
+    m_mid  = OUT_DIR / f"{prefix}_mix_mid.wav"
+    n_head = OUT_DIR / f"{prefix}_next_head.wav"

46-46: Remove the unreachable return

Line 46 repeats the same return float(out.strip()) that already executed inside the try, so it will never run. Please drop the redundant return.

-    return float(out.strip())
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a322414 and 7741409.

📒 Files selected for processing (1)
  • transitions/transition_extraction.py (1 hunks)
🧰 Additional context used
🪛 Ruff (0.13.1)
transitions/transition_extraction.py

30-30: subprocess call: check for execution of untrusted input

(S603)


34-34: subprocess call: check for execution of untrusted input

(S603)


34-39: Starting a process with a partial executable path

(S607)


45-45: Avoid specifying long messages outside the exception class

(TRY003)


71-71: Avoid specifying long messages outside the exception class

(TRY003)


75-75: Avoid specifying long messages outside the exception class

(TRY003)


127-127: Comment contains ambiguous (RIGHT SINGLE QUOTATION MARK). Did you mean ``` (GRAVE ACCENT)?

(RUF003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Sourcery review
  • GitHub Check: Sourcery review

Comment on lines +120 to +133
def safe_crossfade_duration(a_path: Path, b_path: Path, desired_cross: float) -> float:

# Returns a safe crossfade duration that won't exceed the length of either audio file.

len_a = duration(str(a_path))
len_b = duration(str(b_path))

# pick the shortest file’s length * 0.8 to stay safe
max_possible = min(len_a, len_b) * 0.8
if desired_cross > max_possible:
print(f"Crossfade {desired_cross:.2f}s too long for clip pair "
f"({len_a:.2f}s, {len_b:.2f}s). Using {max_possible:.2f}s instead.")
return max_possible
return desired_cross

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Enforce positive crossfade durations

Line 120 currently only clamps the upper bound. If the caller passes cross=0 or the shortest clip ends up zero-length (e.g., pre_tail=0 in extract_segments), safe_crossfade_duration returns 0 and ffmpeg’s acrossfade rejects d<=0, raising CalledProcessError. Guard against non-positive durations before spawning ffmpeg.

@@
-    max_possible = min(len_a, len_b) * 0.8
-    if desired_cross > max_possible:
+    max_possible = min(len_a, len_b) * 0.8
+    if desired_cross <= 0:
+        raise ValueError(f"Crossfade duration must be positive (got {desired_cross}).")
+    if max_possible <= 0:
+        raise ValueError(f"Cannot crossfade clips with no audio ({len_a:.2f}s, {len_b:.2f}s).")
+    if desired_cross > max_possible:
         print(f"Crossfade {desired_cross:.2f}s too long for clip pair "
               f"({len_a:.2f}s, {len_b:.2f}s). Using {max_possible:.2f}s instead.")
         return max_possible
📝 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
def safe_crossfade_duration(a_path: Path, b_path: Path, desired_cross: float) -> float:
# Returns a safe crossfade duration that won't exceed the length of either audio file.
len_a = duration(str(a_path))
len_b = duration(str(b_path))
# pick the shortest file’s length * 0.8 to stay safe
max_possible = min(len_a, len_b) * 0.8
if desired_cross > max_possible:
print(f"Crossfade {desired_cross:.2f}s too long for clip pair "
f"({len_a:.2f}s, {len_b:.2f}s). Using {max_possible:.2f}s instead.")
return max_possible
return desired_cross
def safe_crossfade_duration(a_path: Path, b_path: Path, desired_cross: float) -> float:
# Returns a safe crossfade duration that won't exceed the length of either audio file.
len_a = duration(str(a_path))
len_b = duration(str(b_path))
# pick the shortest file’s length * 0.8 to stay safe
max_possible = min(len_a, len_b) * 0.8
if desired_cross <= 0:
raise ValueError(f"Crossfade duration must be positive (got {desired_cross}).")
if max_possible <= 0:
raise ValueError(f"Cannot crossfade clips with no audio ({len_a:.2f}s, {len_b:.2f}s).")
if desired_cross > max_possible:
print(f"Crossfade {desired_cross:.2f}s too long for clip pair "
f"({len_a:.2f}s, {len_b:.2f}s). Using {max_possible:.2f}s instead.")
return max_possible
return desired_cross
🧰 Tools
🪛 Ruff (0.13.1)

127-127: Comment contains ambiguous (RIGHT SINGLE QUOTATION MARK). Did you mean ``` (GRAVE ACCENT)?

(RUF003)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant