Skip to content

Add Sound.copy and Sound.__copy__ #3556

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: main
Choose a base branch
from

Conversation

oddbookworm
Copy link
Member

@oddbookworm oddbookworm commented Aug 10, 2025

I added https://github.com/Borishkof as a co-author because I took the test and tweaked the docs from their old pull request

Summary by CodeRabbit

  • New Features

    • Added Sound.copy() and Sound.copy to create independent deep copies that preserve volume and return instances of the same subclass; may raise TypeError or pygame.error on failure.
  • Documentation

    • Docs updated to describe copy() behavior and error cases; notes that bytes()/bytearray() use the Sound buffer interface (documentation added in two locations).
  • Tests

    • New tests verify copied sounds match length/channels/raw data, remain independent, and play correctly (including subclass instances).

@oddbookworm oddbookworm requested a review from a team as a code owner August 10, 2025 02:41
@oddbookworm oddbookworm added New API This pull request may need extra debate as it adds a new class or function to pygame mixer pygame.mixer C API labels Aug 10, 2025
@oddbookworm oddbookworm linked an issue Aug 10, 2025 that may be closed by this pull request
@oddbookworm
Copy link
Member Author

image

@ankith26
Copy link
Member

@coderabbitai full review

Copy link

coderabbitai bot commented Aug 13, 2025

✅ Actions performed

Full review triggered.

Copy link

coderabbitai bot commented Aug 13, 2025

Walkthrough

Adds Sound.copy() and Sound.copy to pygame.mixer.Sound with a C implementation that clones a Sound from its raw buffer (preserving volume), updates type stubs and docs, adjusts mixer cleanup semantics, and adds tests verifying copy parity, independence, playback, and subclass preservation.

Changes

Cohort / File(s) Summary
API stubs
buildconfig/stubs/pygame/mixer.pyi
Import Self from typing_extensions and add Sound.copy() -> Self and Sound.__copy__() -> Self.
C implementation & runtime
src_c/mixer.c
Add snd_copy (static PyObject *), forward-declare snd_get_arraystruct, implement buffer-based copy preserving Mix_Chunk volume, wire copy and __copy__ into sound_methods, and change mixer cleanup to halt groups instead of prior DECREF behavior.
Documentation
docs/reST/ref/mixer.rst, src_c/doc/mixer_doc.h
Add DOC_MIXER_SOUND_COPY macro and documentation entries for Sound.copy/__copy__; note that bytes/bytearray use the buffer interface. (Doc entry inserted in two locations.)
Tests
test/mixer_test.py
Add test_snd_copy (imports copy), testing multiple formats and asserting length, channels, volume parity and independence, raw-data equality, playback after original deletion, and subclass copy preservation.

Sequence Diagram(s)

sequenceDiagram
  participant Py as Python
  participant Sound as Sound (self)
  participant C as mixer.c (snd_copy)
  participant Raw as raw buffer
  participant New as New Sound

  Py->>Sound: call copy() / __copy__()
  Sound->>C: invoke snd_copy(self)
  C->>Sound: snd_get_raw(self)
  Sound-->>Raw: return buffer/bytes
  C->>New: allocate new Sound (tp_new)
  C->>New: initialize from buffer via sound_init
  C->>New: set volume to source volume
  C-->>Py: return New
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Poem

I hop and press the copy key,
Two sounds now hum and follow me.
One keeps volume, one stays true,
Subclass echoes — fresh and new.
Rabbit claps — a twin for you! 🐇🎶

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear 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 669f06e and 3902608.

📒 Files selected for processing (1)
  • src_c/mixer.c (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src_c/mixer.c
⏰ 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). (19)
  • GitHub Check: AMD64
  • GitHub Check: x86
  • GitHub Check: dev-check
  • GitHub Check: build (ubuntu-22.04)
  • GitHub Check: msys2 (clang64, clang-x86_64)
  • GitHub Check: msys2 (mingw64, x86_64)
  • GitHub Check: msys2 (ucrt64, ucrt-x86_64)
  • GitHub Check: build (macos-14)
  • GitHub Check: aarch64
  • GitHub Check: build (ubuntu-24.04)
  • GitHub Check: x86_64
  • GitHub Check: i686
  • GitHub Check: Debian (Bookworm - 12) [armv7]
  • GitHub Check: Debian (Bookworm - 12) [ppc64le]
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.14.0rc1)
  • GitHub Check: Debian (Bookworm - 12) [armv6]
  • GitHub Check: Debian (Bookworm - 12) [s390x]
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.13.5)
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.9.23)
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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: 2

🧹 Nitpick comments (3)
buildconfig/stubs/pygame/mixer.pyi (1)

75-76: Use Self as the return type to preserve subclass typing for copy/copy

Across pygame stubs, copy() methods usually return Self (see Surface, Rect, etc.). For consistency and better type inference when subclassing Sound, prefer Self here. If you keep returning Sound, copying a subclass will be typed as base Sound.

Apply this diff:

-from typing_extensions import (
-    Buffer,  # collections.abc 3.12
-    deprecated,  # added in 3.13
-)
+from typing_extensions import (
+    Buffer,  # collections.abc 3.12
+    deprecated,  # added in 3.13
+    Self,
+)
@@
-    def copy(self) -> Sound: ...
-    def __copy__(self) -> Sound: ...
+    def copy(self) -> Self: ...
+    def __copy__(self) -> Self: ...
docs/reST/ref/mixer.rst (1)

507-523: Clarify subclass behavior and add copy/deepcopy note if applicable

If we keep or change the implementation to preserve subclass type on copy (recommended), consider adding a sentence: “The returned object has the same class as the original.” If implementing deepcopy to delegate to copy(), mirror the Surface docs and add that note here as well.

Proposed doc tweak (only if subclass type is preserved):

-      Return a new Sound object that is a deep copy of this Sound. The new Sound
-      will be just as if you loaded it from the same file on disk as you did the
-      original Sound. If the copy fails, a ``TypeError`` or :meth:`pygame.error`
-      exception will be raised.
+      Return a new Sound object that is a deep copy of this Sound. The returned
+      object will have the same class as the original and be just as if you
+      loaded it from the same file on disk. If the copy fails, a ``TypeError``
+      or :meth:`pygame.error` exception will be raised.
src_c/mixer.c (1)

895-897: Consider supporting deepcopy for parity with Surface and Python’s copy protocol

Optional but nice-to-have: implement deepcopy to delegate to copy(), like Surface does, so copy.copy() and copy.deepcopy() both work intuitively.

Add this wrapper function (outside current hunk):

// Add near snd_copy
static PyObject *
snd_deepcopy(PyObject *self, PyObject *memo)
{
    // ignore memo, perform a deep copy identical to copy()
    return snd_copy(self, NULL);
}

Then add this entry to sound_methods:

     {"copy", snd_copy, METH_NOARGS, DOC_MIXER_SOUND_COPY},
     {"__copy__", snd_copy, METH_NOARGS, DOC_MIXER_SOUND_COPY},
+    {"__deepcopy__", snd_deepcopy, METH_O, DOC_MIXER_SOUND_COPY},

And update the stubs and docs accordingly, if adopted.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 94370b6 and 88a9a13.

📒 Files selected for processing (5)
  • buildconfig/stubs/pygame/mixer.pyi (1 hunks)
  • docs/reST/ref/mixer.rst (2 hunks)
  • src_c/doc/mixer_doc.h (1 hunks)
  • src_c/mixer.c (3 hunks)
  • test/mixer_test.py (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
buildconfig/stubs/pygame/mixer.pyi (7)
buildconfig/stubs/pygame/sprite.pyi (1)
  • copy (142-142)
buildconfig/stubs/pygame/surface.pyi (1)
  • copy (335-346)
buildconfig/stubs/pygame/math.pyi (1)
  • copy (62-62)
buildconfig/stubs/pygame/mask.pyi (1)
  • copy (21-21)
buildconfig/stubs/pygame/cursors.pyi (1)
  • copy (83-83)
buildconfig/stubs/pygame/geometry.pyi (2)
  • copy (138-138)
  • copy (175-175)
buildconfig/stubs/pygame/rect.pyi (1)
  • copy (152-152)
test/mixer_test.py (3)
src_c/mixer.c (3)
  • mixer (2088-2179)
  • get_num_channels (1455-1460)
  • get_busy (1509-1517)
buildconfig/stubs/pygame/mixer.pyi (20)
  • init (16-23)
  • get_sdl_mixer_version (46-46)
  • Sound (48-76)
  • copy (75-75)
  • get_length (73-73)
  • get_num_channels (40-40)
  • get_num_channels (72-72)
  • get_volume (71-71)
  • get_volume (99-99)
  • get_raw (74-74)
  • set_volume (70-70)
  • set_volume (96-96)
  • set_volume (98-98)
  • play (53-58)
  • play (82-88)
  • get_busy (45-45)
  • get_busy (100-100)
  • stop (35-35)
  • stop (68-68)
  • stop (89-89)
test/test_utils/__init__.py (1)
  • example_path (36-37)
🪛 Flake8 (7.2.0)
test/mixer_test.py

[error] 1350-1350: local variable 'e' is assigned to but never used

(F841)

🔇 Additional comments (4)
src_c/doc/mixer_doc.h (1)

29-29: Docs addition looks good and matches the new API

The macro clearly documents both Sound.copy() and copy.copy(original_sound), aligned with the implementation.

test/mixer_test.py (1)

1333-1372: Good coverage of Sound.copy parity and independence

The test exercises core invariants: length, channels, volume, raw bytes parity; volume independence; basic playback on the copy; and conditional MP3 coverage. Looks solid.

docs/reST/ref/mixer.rst (1)

372-375: Nice clarification about bytes(Sound) and buffer interface

This note preempts confusion about bytes and explains the existing buffer interface well.

src_c/mixer.c (1)

92-94: Forward declaration is fine

Adding the forward declaration fixes ordering and avoids compiler warnings when referencing the getter in sound_getset.

@oddbookworm
Copy link
Member Author

@coderabbitai please review the changes now

Copy link

coderabbitai bot commented Aug 13, 2025

Tip

For best results, initiate chat on the files or code changes.

@oddbookworm I'll provide a comprehensive review of your Sound.copy implementation. This is a well-structured PR that adds copy functionality to pygame.mixer.Sound objects.

🎯 Overall Assessment

Excellent implementation! The PR demonstrates solid software engineering practices with comprehensive coverage across type stubs, documentation, C implementation, and tests.

📋 Detailed Review

Type Stubs (buildconfig/stubs/pygame/mixer.pyi)

The type annotations are correct and follow Python typing conventions:

def copy(self) -> Sound: ...
def __copy__(self) -> Sound: ...

C Implementation (src_c/mixer.c)

The snd_copy implementation is robust:

  • Properly handles memory allocation and error cases
  • Uses snd_get_raw to access the underlying buffer data
  • Raises appropriate SDLError on failure
  • Correctly implements both copy() and __copy__() methods

Documentation (docs/reST/ref/mixer.rst)

Well-documented with:

  • Clear method signature
  • Proper version information (versionadded 2.5.6)
  • Examples showing both direct call and copy.copy() usage
  • Appropriate exception documentation

Tests (test/mixer_test.py)

Comprehensive test coverage that:

  • Tests multiple audio formats (including conditional MP3 support)
  • Verifies independence between original and copy
  • Tests all key properties (length, channels, volume, raw data)
  • Includes playback verification
  • Gracefully handles unsupported formats

🔍 Minor Observations

  1. Documentation duplication: The copy method documentation appears twice in the RST file - this might be intentional for different sections, but worth verifying.

  2. Error handling: The C implementation properly raises SDLError when copy fails, which aligns with pygame conventions.

  3. Memory management: The implementation correctly handles the buffer data transfer without memory leaks.

🏆 Strengths

  • Complete implementation: Covers all necessary components
  • Python protocol compliance: Properly implements __copy__ for standard library compatibility
  • Robust error handling: Appropriate exception handling throughout
  • Thorough testing: Tests cover edge cases and verify independence
  • Good documentation: Clear usage examples and version information

🎉 Recommendation

APPROVED - This is a high-quality implementation that adds valuable functionality while maintaining code quality standards. The deep copy semantics are well-implemented and properly tested.

Great work on this comprehensive addition to pygame-ce! 🐰

  🐰
 /   \
( ^.^ )
 > ^ <

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: 1

🔭 Outside diff range comments (1)
src_c/mixer.c (1)

298-305: Refcount leak in endsound_callback: missing DECREF of current sound

When a channel finishes or dequeues to the next sound, channeldata[channel].sound is reassigned or set to NULL without a corresponding Py_XDECREF, leaking a reference per playback.

Apply this diff:

         if (channeldata[channel].queue) {
             PyGILState_STATE gstate = PyGILState_Ensure();
             int channelnum;
             Mix_Chunk *sound = pgSound_AsChunk(channeldata[channel].queue);
+            Py_XDECREF(channeldata[channel].sound);
             channeldata[channel].sound = channeldata[channel].queue;
             channeldata[channel].queue = NULL;
             PyGILState_Release(gstate);
             channelnum = Mix_PlayChannelTimed(channel, sound, 0, -1);
             if (channelnum != -1) {
                 Mix_GroupChannel(channelnum, (int)(intptr_t)sound);
             }
         }
         else {
             PyGILState_STATE gstate = PyGILState_Ensure();
+            Py_XDECREF(channeldata[channel].sound);
             channeldata[channel].sound = NULL;
             PyGILState_Release(gstate);
             Mix_GroupChannel(channel, -1);
         }

Also applies to: 311-315

♻️ Duplicate comments (2)
src_c/mixer.c (2)

810-812: Fix subclass-unsafe tp_new call: pass Py_TYPE(self) to tp_new, and NULL-check the allocation

Using the subclass’s tp_new with the base type argument forces base-type allocation and can drop subclass type. Also, check allocation failure.

Apply this diff:

-    pgSoundObject *newSound =
-        (pgSoundObject *)Py_TYPE(self)->tp_new(&pgSound_Type, NULL, NULL);
+    pgSoundObject *newSound =
+        (pgSoundObject *)Py_TYPE(self)->tp_new(Py_TYPE(self), NULL, NULL);
+    if (newSound == NULL) {
+        // tp_new has already set an exception
+        return NULL;
+    }

813-836: Handle allocation failures, simplify kwargs insertion, and fix newSound leak on error paths

  • kwargs is not checked for NULL (OOM).
  • Creating a separate key object is unnecessary; use PyDict_SetItemString.
  • On errors (bytes == NULL or dict insertion failure), newSound is leaked; DECREF it.

Apply this diff:

-    PyObject *kwargs = PyDict_New();
-    PyObject *key = PyUnicode_FromString("buffer");
-    PyObject *bytes = snd_get_raw(self, NULL);
+    PyObject *kwargs = PyDict_New();
+    if (kwargs == NULL) {
+        Py_DECREF(newSound);
+        return NULL;
+    }
+    PyObject *bytes = snd_get_raw(self, NULL);
     if (bytes == NULL) {
-        // exception set already by PyBytes_FromStringAndSize
+        // exception set already by snd_get_raw / PyBytes_FromStringAndSize
         PG_SAVE_EXCEPTION
-        Py_DECREF(key);
         Py_DECREF(kwargs);
+        Py_DECREF(newSound);
         PG_UNSAVE_EXCEPTION
         return NULL;
     }
 
-    if (PyDict_SetItem(kwargs, key, bytes) < 0) {
+    if (PyDict_SetItemString(kwargs, "buffer", bytes) < 0) {
         // exception set already
         PG_SAVE_EXCEPTION
-        Py_DECREF(key);
         Py_DECREF(bytes);
         Py_DECREF(kwargs);
+        Py_DECREF(newSound);
         PG_UNSAVE_EXCEPTION
         return NULL;
     }
-    Py_DECREF(key);
     Py_DECREF(bytes);
🧹 Nitpick comments (1)
src_c/mixer.c (1)

907-909: Method wiring LGTM; consider adding a subclass retention test

copy and copy entries are correctly exposed and share the same implementation/doc. Given the tp_new fix above, please add a regression test ensuring that copying a subclass of Sound returns an instance of the same subclass.

Example test sketch:

  • Define a trivial subclass of pygame.mixer.Sound
  • Construct from a small in-memory buffer
  • Assert type(copy()) is the subclass
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 88a9a13 and 12c9952.

📒 Files selected for processing (2)
  • src_c/mixer.c (3 hunks)
  • test/mixer_test.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/mixer_test.py
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: oddbookworm
PR: pygame-community/pygame-ce#3556
File: src_c/mixer.c:806-836
Timestamp: 2025-08-13T12:39:19.107Z
Learning: In pygame-ce's mixer.c, the snd_copy function requires Py_INCREF(newSound) before returning to maintain proper reference counting. Without it, copied Sound objects get deallocated prematurely.
📚 Learning: 2025-08-13T12:39:19.107Z
Learnt from: oddbookworm
PR: pygame-community/pygame-ce#3556
File: src_c/mixer.c:806-836
Timestamp: 2025-08-13T12:39:19.107Z
Learning: In pygame-ce's mixer.c, the snd_copy function requires Py_INCREF(newSound) before returning to maintain proper reference counting. Without it, copied Sound objects get deallocated prematurely.

Applied to files:

  • src_c/mixer.c
⏰ 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). (20)
  • GitHub Check: AMD64
  • GitHub Check: x86
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.14.0rc1)
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.13.5)
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.9.23)
  • GitHub Check: Debian (Bookworm - 12) [armv6]
  • GitHub Check: x86_64
  • GitHub Check: Debian (Bookworm - 12) [s390x]
  • GitHub Check: Debian (Bookworm - 12) [ppc64le]
  • GitHub Check: aarch64
  • GitHub Check: i686
  • GitHub Check: Debian (Bookworm - 12) [armv7]
  • GitHub Check: dev-check
  • GitHub Check: msys2 (clang64, clang-x86_64)
  • GitHub Check: build (macos-14)
  • GitHub Check: msys2 (ucrt64, ucrt-x86_64)
  • GitHub Check: build (ubuntu-24.04)
  • GitHub Check: build (windows-latest)
  • GitHub Check: msys2 (mingw64, x86_64)
  • GitHub Check: build (ubuntu-22.04)
🔇 Additional comments (1)
src_c/mixer.c (1)

92-93: Forward declaration looks fine

The forward declaration for snd_get_arraystruct matches the implementation signature below and avoids implicit declaration warnings. No issues.

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

🧹 Nitpick comments (3)
buildconfig/stubs/pygame/mixer.pyi (1)

75-76: Return Self for copy/copy to reflect subtype-preserving semantics

The runtime implementation preserves the original subclass type by using Py_TYPE(self)->tp_new(...). The stubs should model that by returning Self instead of Sound.

Apply this diff:

-    def copy(self) -> Sound: ...
-    def __copy__(self) -> Sound: ...
+    def copy(self) -> Self: ...
+    def __copy__(self) -> Self: ...

And add Self to the imports (outside this hunk), for example:

from typing_extensions import Buffer, deprecated, Self  # add Self
test/mixer_test.py (2)

1352-1356: Ensure all filenames are exercised even when MP3 is added

zip(filenames, old_volumes, new_volumes) truncates to the shortest sequence. When MP3 is appended, the last file isn’t tested. Iterate by index or cycle volumes so every file is covered.

One simple refactor:

-        for f, old_vol, new_vol in zip(filenames, old_volumes, new_volumes):
-            filename = example_path(os.path.join("data", f))
+        for i, f in enumerate(filenames):
+            filename = example_path(os.path.join("data", f))
+            old_vol = old_volumes[i % len(old_volumes)]
+            new_vol = new_volumes[i % len(new_volumes)]

Apply the same change to the subclass loop below.


1333-1409: Solid coverage of copy semantics and independence

  • Validates length, channels, volume, and raw data parity between original and copy.
  • Confirms independent volume after mutation.
  • Exercises playback on the copy and ensures channel accounting is sane.
  • Extends coverage to a Sound subclass.

Consider adding a quick check for copy.copy(sound) to exercise __copy__ as well.

Example addition outside this hunk:

import copy
# ...
cpy = copy.copy(sound)
self.assertIsInstance(cpy, mixer.Sound)
self.assertEqual(sound.get_raw(), cpy.get_raw())
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear 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 12c9952 and 9f82126.

📒 Files selected for processing (5)
  • buildconfig/stubs/pygame/mixer.pyi (1 hunks)
  • docs/reST/ref/mixer.rst (2 hunks)
  • src_c/doc/mixer_doc.h (1 hunks)
  • src_c/mixer.c (4 hunks)
  • test/mixer_test.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src_c/doc/mixer_doc.h
  • docs/reST/ref/mixer.rst
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: oddbookworm
PR: pygame-community/pygame-ce#3556
File: src_c/mixer.c:806-836
Timestamp: 2025-08-13T12:39:19.107Z
Learning: In pygame-ce's mixer.c, the snd_copy function requires Py_INCREF(newSound) before returning to maintain proper reference counting. Without it, copied Sound objects get deallocated prematurely.
📚 Learning: 2025-08-13T12:39:19.107Z
Learnt from: oddbookworm
PR: pygame-community/pygame-ce#3556
File: src_c/mixer.c:806-836
Timestamp: 2025-08-13T12:39:19.107Z
Learning: In pygame-ce's mixer.c, the snd_copy function requires Py_INCREF(newSound) before returning to maintain proper reference counting. Without it, copied Sound objects get deallocated prematurely.

Applied to files:

  • test/mixer_test.py
  • buildconfig/stubs/pygame/mixer.pyi
  • src_c/mixer.c
🧬 Code Graph Analysis (1)
test/mixer_test.py (3)
buildconfig/stubs/pygame/mixer.pyi (17)
  • Sound (48-76)
  • init (16-23)
  • set_volume (70-70)
  • set_volume (96-96)
  • set_volume (98-98)
  • copy (75-75)
  • get_length (73-73)
  • get_num_channels (40-40)
  • get_num_channels (72-72)
  • get_volume (71-71)
  • get_volume (99-99)
  • get_raw (74-74)
  • play (53-58)
  • play (82-88)
  • stop (35-35)
  • stop (68-68)
  • stop (89-89)
test/test_utils/__init__.py (1)
  • example_path (36-37)
buildconfig/stubs/pygame/base.pyi (1)
  • error (6-6)
⏰ 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). (3)
  • GitHub Check: Debian (Bookworm - 12) [build - armv7, test - armv6]
  • GitHub Check: x86_64
  • GitHub Check: arm64
🔇 Additional comments (4)
src_c/mixer.c (4)

92-94: LGTM: forward declaration is fine

The static forward declaration for snd_get_arraystruct is appropriate and matches the later definition.


852-860: Nice touch: copy preserves the original chunk’s volume

Mirroring the source volume onto the new chunk maintains parity and matches user expectations. Good call.


861-863: Confirm refcounting: no extra INCREF should be needed when returning newSound

Returning the result of tp_new with refcount 1 is the normal pattern; an extra INCREF here would leak. However, a prior debug build report mentioned undercount on 3.14 rc. Please re-validate on your target build to ensure no premature deallocation occurs before merging.

If you still observe undercounting with a Python debug build:

  • Share a minimal reproducer that drops the copy too early.
  • We can revisit whether an INCREF is warranted in this specific code path.

922-924: Public API surface looks correct

Exposing both copy() and __copy__() to back the Python copy protocol is consistent and ergonomic.

src_c/mixer.c Outdated
Comment on lines 518 to 527
if (channeldata) {
for (i = 0; i < numchanneldata; ++i) {
Py_XDECREF(channeldata[i].sound);
if (channeldata[i].queue) {
Mix_HaltGroup(
(int)(intptr_t)pgSound_AsChunk(channeldata[i].sound));
}
else {
Mix_HaltGroup(-1);
}
Py_XDECREF(channeldata[i].queue);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Leak: channeldata[i].sound is never decref’d in mixer_quit

Inside the loop, channeldata[i].sound holds a reference to a Python object. The code decrefs queue but never decrefs sound, leaking a ref per channel. Add Py_XDECREF(channeldata[i].sound); before freeing channeldata.

Apply this diff:

                 if (channeldata[i].queue) {
                     Mix_HaltGroup(
                         (int)(intptr_t)pgSound_AsChunk(channeldata[i].sound));
                 }
                 else {
                     Mix_HaltGroup(-1);
                 }
+                Py_XDECREF(channeldata[i].sound);
                 Py_XDECREF(channeldata[i].queue);

Nit (optional): Calling Mix_HaltGroup(-1) inside the loop repeats the same work N times; you can do it once outside the loop for the non-queue case.

📝 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
if (channeldata) {
for (i = 0; i < numchanneldata; ++i) {
Py_XDECREF(channeldata[i].sound);
if (channeldata[i].queue) {
Mix_HaltGroup(
(int)(intptr_t)pgSound_AsChunk(channeldata[i].sound));
}
else {
Mix_HaltGroup(-1);
}
Py_XDECREF(channeldata[i].queue);
if (channeldata) {
for (i = 0; i < numchanneldata; ++i) {
if (channeldata[i].queue) {
Mix_HaltGroup(
(int)(intptr_t)pgSound_AsChunk(channeldata[i].sound));
}
else {
Mix_HaltGroup(-1);
}
Py_XDECREF(channeldata[i].sound);
Py_XDECREF(channeldata[i].queue);
🤖 Prompt for AI Agents
In src_c/mixer.c around lines 518 to 527, the loop in mixer_quit decrefs
channeldata[i].queue but never decrefs channeldata[i].sound, leaking one Python
reference per channel; add a Py_XDECREF(channeldata[i].sound) for each iteration
before you free channeldata. Also, to avoid repeating work, call
Mix_HaltGroup(-1) once outside the loop for the non-queue case instead of
calling it on every iteration. Ensure the added Py_XDECREF is used (not
Py_DECREF) to safely handle NULL values.

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: 0

♻️ Duplicate comments (1)
src_c/mixer.c (1)

520-527: Fix refcount leak in mixer_quit: DECREF channeldata[i].sound

We halt groups and DECREF queue, but never decref the per-channel Python Sound reference. This leaks one Python ref per channel on quit.

Apply this diff:

                 if (channeldata[i].queue) {
                     Mix_HaltGroup(
                         (int)(intptr_t)pgSound_AsChunk(channeldata[i].sound));
                 }
                 else {
                     Mix_HaltGroup(-1);
                 }
+                Py_XDECREF(channeldata[i].sound);
                 Py_XDECREF(channeldata[i].queue);

Optional nit: Mix_HaltGroup(-1) inside the loop repeats work N times. Consider hoisting it outside the loop for the non-queue case.

🧹 Nitpick comments (2)
src_c/mixer.c (1)

810-879: Avoid double-copying large audio buffers (optional)

Current flow copies audio data twice: (1) Sound -> PyBytes, (2) PyBytes -> new Mix_Chunk memory. If you want to optimize large-copy paths, you could allocate the destination chunk and memcpy directly from orig->abuf. That would keep deep-copy semantics but with one fewer allocation/copy.

If you want, I can sketch a variant that bypasses the intermediate PyBytes and calls a small helper parallel to _chunk_from_buf.

test/mixer_test.py (1)

1334-1441: Thorough coverage; consider guarding playback asserts under “disk” driver and using subTests

Great validation across formats, method paths (copy() and copy.copy()), and subclass preservation. Two small improvements to reduce flakiness and improve diagnostics:

  • The “disk” audio driver can make get_busy() assertions unreliable. Guard the playback assertions when SDL_AUDIODRIVER=disk (similar to other tests).
  • Use subTest per file to pinpoint failures faster.

Apply this minimal guard in each playback block:

-            channel = sound_copy.play()
+            channel = sound_copy.play()
             if channel is None:
                 continue
+            if os.environ.get("SDL_AUDIODRIVER") == "disk":
+                continue
             self.assertTrue(channel.get_busy())

Optionally wrap each (f, old_vol, new_vol) iteration with self.subTest(file=f) for clearer failure reporting.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear 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 9f82126 and eae8383.

📒 Files selected for processing (3)
  • buildconfig/stubs/pygame/mixer.pyi (2 hunks)
  • src_c/mixer.c (4 hunks)
  • test/mixer_test.py (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • buildconfig/stubs/pygame/mixer.pyi
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: oddbookworm
PR: pygame-community/pygame-ce#3556
File: src_c/mixer.c:806-836
Timestamp: 2025-08-13T12:39:19.107Z
Learning: In pygame-ce's mixer.c, the snd_copy function requires Py_INCREF(newSound) before returning to maintain proper reference counting. Without it, copied Sound objects get deallocated prematurely.
📚 Learning: 2025-08-13T12:39:19.107Z
Learnt from: oddbookworm
PR: pygame-community/pygame-ce#3556
File: src_c/mixer.c:806-836
Timestamp: 2025-08-13T12:39:19.107Z
Learning: In pygame-ce's mixer.c, the snd_copy function requires Py_INCREF(newSound) before returning to maintain proper reference counting. Without it, copied Sound objects get deallocated prematurely.

Applied to files:

  • test/mixer_test.py
  • src_c/mixer.c
🧬 Code Graph Analysis (1)
test/mixer_test.py (3)
buildconfig/stubs/pygame/mixer.pyi (18)
  • copy (76-76)
  • Sound (49-77)
  • init (17-24)
  • get_sdl_mixer_version (47-47)
  • set_volume (71-71)
  • set_volume (97-97)
  • set_volume (99-99)
  • get_length (74-74)
  • get_num_channels (41-41)
  • get_num_channels (73-73)
  • get_volume (72-72)
  • get_volume (100-100)
  • get_raw (75-75)
  • play (54-59)
  • play (83-89)
  • stop (36-36)
  • stop (69-69)
  • stop (90-90)
test/test_utils/__init__.py (1)
  • example_path (36-37)
buildconfig/stubs/pygame/base.pyi (1)
  • error (6-6)
⏰ 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). (20)
  • GitHub Check: arm64 deps
  • GitHub Check: msys2 (clang64, clang-x86_64)
  • GitHub Check: msys2 (mingw64, x86_64)
  • GitHub Check: msys2 (ucrt64, ucrt-x86_64)
  • GitHub Check: Debian (Bookworm - 12) [s390x]
  • GitHub Check: Debian (Bookworm - 12) [armv7]
  • GitHub Check: Debian (Bookworm - 12) [ppc64le]
  • GitHub Check: Debian (Bookworm - 12) [armv6]
  • GitHub Check: build (ubuntu-24.04)
  • GitHub Check: i686
  • GitHub Check: build (macos-14)
  • GitHub Check: aarch64
  • GitHub Check: x86_64
  • GitHub Check: AMD64
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.13.5)
  • GitHub Check: dev-check
  • GitHub Check: x86
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.9.23)
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.14.0rc1)
  • GitHub Check: build (ubuntu-22.04)
🔇 Additional comments (4)
src_c/mixer.c (3)

92-93: Forward declaration is appropriate

Adding the static forward declaration for snd_get_arraystruct resolves ordering/visibility and avoids implicit-int-style warnings. Looks good.


810-879: Sound.copy implementation is correct and robust

Highlights:

  • Preserves subclass type via Py_TYPE(self)->tp_new — good.
  • Proper NULL checks and full error-path cleanup (no leaks).
  • Uses snd_get_raw + sound_init(buffer=...) to deep-copy bytes — simple and consistent.
  • Preserves original chunk volume on the copy — nice parity touch.
  • Correctly returns the new reference without an extra INCREF.

Re: refcounting — returning (PyObject *)newSound after tp_new + successful init is correct; adding a Py_INCREF here would leak a reference.


938-940: API surface wiring for copy/copy looks correct

Method table exports both .copy() and copy to Python with the shared implementation and appropriate doc macro. Good addition and Python protocol compliance.

test/mixer_test.py (1)

1-1: LGTM: needed import for copy.copy

Import is required for exercising copy via copy.copy(). All good.

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: 0

♻️ Duplicate comments (1)
src_c/mixer.c (1)

520-528: Leak: channeldata[i].sound is never decref’d in mixer_quit

The loop decrefs queue but not sound, leaking a Python ref per channel. Add a DECREF for channeldata[i].sound on each iteration.

Apply this minimal fix:

                 if (channeldata[i].queue) {
                     Mix_HaltGroup(
                         (int)(intptr_t)pgSound_AsChunk(channeldata[i].sound));
                 }
                 else {
                     Mix_HaltGroup(-1);
                 }
+                Py_XDECREF(channeldata[i].sound);
                 Py_XDECREF(channeldata[i].queue);

Optional nit: calling Mix_HaltGroup(-1) once before the loop (instead of in every else) avoids redundant work. I can draft that change if you’d like.

🧹 Nitpick comments (1)
src_c/mixer.c (1)

816-886: Sound.copy implementation: preserves subclass, propagates errors, and keeps volume — LGTM

Highlights:

  • Constructs via Py_TYPE(self)->tp_new to preserve subclass type.
  • Robust error handling and cleanup across all failure paths (including snd_get_raw and PyDict_SetItemString).
  • Calls sound_init with a buffer kwarg; exceptions are propagated (good use of PG_SAVE/UNSAVE).
  • Copies Mix_Chunk volume from original to copy to maintain parity.
  • Returns a new reference without leaking.

Minor nit only: consider renaming dict to kwargs for readability since it’s used as keyword args for sound_init. No functional change needed.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear 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 eae8383 and b3ff4cc.

📒 Files selected for processing (1)
  • src_c/mixer.c (7 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: oddbookworm
PR: pygame-community/pygame-ce#3556
File: src_c/mixer.c:806-836
Timestamp: 2025-08-13T12:39:19.107Z
Learning: In pygame-ce's mixer.c, the snd_copy function requires Py_INCREF(newSound) before returning to maintain proper reference counting. Without it, copied Sound objects get deallocated prematurely.
📚 Learning: 2025-08-13T12:39:19.107Z
Learnt from: oddbookworm
PR: pygame-community/pygame-ce#3556
File: src_c/mixer.c:806-836
Timestamp: 2025-08-13T12:39:19.107Z
Learning: In pygame-ce's mixer.c, the snd_copy function requires Py_INCREF(newSound) before returning to maintain proper reference counting. Without it, copied Sound objects get deallocated prematurely.

Applied to files:

  • src_c/mixer.c
⏰ 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). (20)
  • GitHub Check: Debian (Bookworm - 12) [armv7]
  • GitHub Check: Debian (Bookworm - 12) [ppc64le]
  • GitHub Check: Debian (Bookworm - 12) [s390x]
  • GitHub Check: Debian (Bookworm - 12) [armv6]
  • GitHub Check: build (macos-14)
  • GitHub Check: build (ubuntu-24.04)
  • GitHub Check: x86_64
  • GitHub Check: i686
  • GitHub Check: build (windows-latest)
  • GitHub Check: aarch64
  • GitHub Check: x86
  • GitHub Check: AMD64
  • GitHub Check: dev-check
  • GitHub Check: msys2 (ucrt64, ucrt-x86_64)
  • GitHub Check: msys2 (clang64, clang-x86_64)
  • GitHub Check: msys2 (mingw64, x86_64)
  • GitHub Check: build (ubuntu-22.04)
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.9.23)
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.13.5)
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.14.0rc1)
🔇 Additional comments (4)
src_c/mixer.c (4)

92-93: Forward declaration for snd_get_arraystruct looks correct

Prototype matches the definition later; unblocks usage in sound_getset. No issues.


291-319: Switch to Py_DECREF in endsound_callback is safe here

Given queue is only non-NULL when a sound is currently playing (and callback fires on channel end), channeldata[channel].sound should never be NULL in either branch. Using Py_DECREF avoids silently ignoring logic errors. Good tightening.

If you want to be extra defensive in debug builds, consider an assertion before Py_DECREF to document this invariant.


944-946: Expose both copy() and copy() in the method table — good API ergonomics

This aligns with Python’s copy protocol and keeps the direct method. Ensure docs/stubs reflect Self returns (which your PR notes indicate). Looks good.


681-685: Confirmed: Mix_HaltGroup performs synchronous DECREF via endsound_callback

Verification of the SDL_mixer documentation shows that Mix_HaltChannel (and by extension Mix_HaltGroup) invokes any registered Mix_ChannelFinished callback for halted channels before the halt call returns, running under the audio lock. This callback (endsound_callback) will perform the DECREF, so removing the direct DECREF here is safe and will not leak or race.

No code changes needed.

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: 1

♻️ Duplicate comments (1)
src_c/mixer.c (1)

522-529: Leak on shutdown: channeldata[i].sound is never decref’d (and redundant group halts in loop)

The loop DECREFs queue but not sound. This leaks one Python ref per channel on mixer quit. Also, calling Mix_HaltGroup(-1) inside the loop repeats the same global halt N times (minor perf nit).

Apply at least the DECREF fix; consider hoisting the global halt outside the loop.

Proposed diff:

                 if (channeldata[i].queue) {
                     Mix_HaltGroup(
                         (int)(intptr_t)pgSound_AsChunk(channeldata[i].sound));
                 }
                 else {
                     Mix_HaltGroup(-1);
                 }
+                Py_XDECREF(channeldata[i].sound);
                 Py_XDECREF(channeldata[i].queue);

Nit (optional): Move Mix_HaltGroup(-1) out of the loop and call it once for the non-queue case.

🧹 Nitpick comments (1)
src_c/mixer.c (1)

302-314: Switch to Py_DECREF assumes non-NULL; safeguard or assert invariant

endsound_callback likely runs only when a Sound was playing, so channeldata[channel].sound should be non-NULL. If that invariant can ever be violated (edge cases, future changes), Py_DECREF(NULL) will crash.

If you want robustness without sacrificing intent, either add an assert or use Py_XDECREF here.

Proposed diff:

-            Py_DECREF(channeldata[channel].sound);
+            Py_XDECREF(channeldata[channel].sound);
...
-            Py_DECREF(channeldata[channel].sound);
+            Py_XDECREF(channeldata[channel].sound);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear 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 b3ff4cc and 4e11fad.

📒 Files selected for processing (1)
  • src_c/mixer.c (7 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: oddbookworm
PR: pygame-community/pygame-ce#3556
File: src_c/mixer.c:806-836
Timestamp: 2025-08-13T12:39:19.107Z
Learning: In pygame-ce's mixer.c, the snd_copy function requires Py_INCREF(newSound) before returning to maintain proper reference counting. Without it, copied Sound objects get deallocated prematurely.
📚 Learning: 2025-08-13T12:39:19.107Z
Learnt from: oddbookworm
PR: pygame-community/pygame-ce#3556
File: src_c/mixer.c:806-836
Timestamp: 2025-08-13T12:39:19.107Z
Learning: In pygame-ce's mixer.c, the snd_copy function requires Py_INCREF(newSound) before returning to maintain proper reference counting. Without it, copied Sound objects get deallocated prematurely.

Applied to files:

  • src_c/mixer.c
⏰ 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). (18)
  • GitHub Check: AMD64
  • GitHub Check: Debian (Bookworm - 12) [armv6]
  • GitHub Check: Debian (Bookworm - 12) [armv7]
  • GitHub Check: Debian (Bookworm - 12) [ppc64le]
  • GitHub Check: Debian (Bookworm - 12) [s390x]
  • GitHub Check: aarch64
  • GitHub Check: x86_64
  • GitHub Check: i686
  • GitHub Check: dev-check
  • GitHub Check: msys2 (clang64, clang-x86_64)
  • GitHub Check: msys2 (ucrt64, ucrt-x86_64)
  • GitHub Check: msys2 (mingw64, x86_64)
  • GitHub Check: build (ubuntu-22.04)
  • GitHub Check: build (ubuntu-24.04)
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.14.0rc1)
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.13.5)
  • GitHub Check: build (macos-14)
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.9.23)
🔇 Additional comments (3)
src_c/mixer.c (3)

92-93: Good forward declaration to satisfy prototype usage

Adding the snd_get_arraystruct prototype avoids implicit declarations when referenced earlier (e.g., in getset) and keeps C99/C11 compilers happy.


816-885: snd_copy: Solid implementation (subclass-preserving, safe error paths, preserves volume)

Highlights:

  • Preserves subclass type via Py_TYPE(self)->tp_new.
  • Robust NULL/error checks with proper cleanup and exception preservation.
  • Uses snd_get_raw + sound_init and then mirrors the original chunk’s volume onto the copy.

Returning newSound without an extra INCREF is correct — tp_new returns a new reference and tp_init does not consume it.


944-946: Method table wiring for copy and copy looks good

Exposes both copy and __copy__ backed by the same implementation with proper doc macro.

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: 1

🧹 Nitpick comments (2)
src_c/mixer.c (2)

302-306: Double-check DECREF safety in endsound_callback

These paths now unconditionally Py_DECREF(channeldata[channel].sound). In practice, this should always be non-NULL when the callback fires, but if an edge case slips through, this will crash in release builds.

  • If you want the safety of guarding against unexpected NULL without changing semantics, prefer Py_XDECREF here; or
  • Keep Py_DECREF but add an assert to catch invariants in debug builds.

Apply this minimal safety tweak if you prefer robustness over strictness:

-            Py_DECREF(channeldata[channel].sound);
+            Py_XDECREF(channeldata[channel].sound);

And in the else-path:

-            Py_DECREF(channeldata[channel].sound);
+            Py_XDECREF(channeldata[channel].sound);

Also applies to: 312-317


829-839: Nit: kwargs naming

dict here is acting as kwargs for sound_init. Consider renaming to kwargs for clarity, in line with surrounding code.

-    PyObject *dict = PyDict_New();
+    PyObject *kwargs = PyDict_New();

And adjust subsequent references accordingly.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear 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 4e11fad and 0a65766.

📒 Files selected for processing (1)
  • src_c/mixer.c (6 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: oddbookworm
PR: pygame-community/pygame-ce#3556
File: src_c/mixer.c:806-836
Timestamp: 2025-08-13T12:39:19.107Z
Learning: In pygame-ce's mixer.c, the snd_copy function requires Py_INCREF(newSound) before returning to maintain proper reference counting. Without it, copied Sound objects get deallocated prematurely.
📚 Learning: 2025-08-13T12:39:19.107Z
Learnt from: oddbookworm
PR: pygame-community/pygame-ce#3556
File: src_c/mixer.c:806-836
Timestamp: 2025-08-13T12:39:19.107Z
Learning: In pygame-ce's mixer.c, the snd_copy function requires Py_INCREF(newSound) before returning to maintain proper reference counting. Without it, copied Sound objects get deallocated prematurely.

Applied to files:

  • src_c/mixer.c
⏰ 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). (15)
  • GitHub Check: aarch64
  • GitHub Check: x86_64
  • GitHub Check: i686
  • GitHub Check: dev-check
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.14.0rc1)
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.9.23)
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.13.5)
  • GitHub Check: arm64
  • GitHub Check: x86_64
  • GitHub Check: x86
  • GitHub Check: AMD64
  • GitHub Check: Debian (Bookworm - 12) [armv7]
  • GitHub Check: Debian (Bookworm - 12) [armv6]
  • GitHub Check: Debian (Bookworm - 12) [s390x]
  • GitHub Check: Debian (Bookworm - 12) [ppc64le]
🔇 Additional comments (7)
src_c/mixer.c (7)

92-93: Forward declaration looks good

Declaring snd_get_arraystruct before use eliminates prototype warnings and keeps the getset table cleanly typed.


818-827: Subclass-preserving allocation and error handling: LGTM

Using Py_TYPE(self)->tp_new preserves subclass type. The NULL checks and error setting are correct and defensive.


840-858: Robust error paths on snd_get_raw and dict population

Good use of PG_SAVE_EXCEPTION/PG_UNSAVE_EXCEPTION and proper DECREF on all failure paths. Using PyDict_SetItemString avoids a temporary key allocation.


861-869: Propagate sound_init errors correctly

The code cleanly propagates sound_init’s exception and avoids masking with a secondary error. DECREF ordering is correct.


870-877: Nice: preserve original chunk volume

Mirroring the chunk volume keeps copy() semantically aligned with the original Sound state. Good call.


879-881: Refcount on return: please re-verify whether an extra INCREF is required

Returning a newly created PyObject normally should not require an extra Py_INCREF. However, previous discussion on this PR mentioned undercounting observed with a debug build (Py 3.14.0rc1) unless an extra INCREF was added in snd_copy. Current code returns without INCREF.

Before we lock this in, can you re-run the failing scenario you had previously and confirm that no premature deallocation occurs with the current implementation? If it still undercounts, we need to add a balanced INCREF; if not, we should keep it as-is to avoid a leak.

If verification indicates undercounting, apply:

-    Py_DECREF(dict);
-    return (PyObject *)newSound;
+    Py_DECREF(dict);
+    Py_INCREF(newSound);  // Only if undercount is reproducible; otherwise omit
+    return (PyObject *)newSound;

Please share the exact reproducer (Python version, debug flags, minimal script) if you still observe the undercount so we can document the rationale in a code comment.


940-942: Method registrations for copy and copy look correct

Both methods mapped to the same implementation with shared docs; aligns with Python’s copy protocol.

Comment on lines 520 to +529
if (channeldata) {
for (i = 0; i < numchanneldata; ++i) {
Py_XDECREF(channeldata[i].sound);
Py_XDECREF(channeldata[i].queue);
if (channeldata[i].sound) {
Mix_HaltGroup(
(int)(intptr_t)pgSound_AsChunk(channeldata[i].sound));
}
if (channeldata[i].queue) {
Mix_HaltGroup(
(int)(intptr_t)pgSound_AsChunk(channeldata[i].queue));
}
Copy link

@coderabbitai coderabbitai bot Aug 16, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

Leak: channeldata[i].sound and .queue never DECREF’d in mixer_quit

Each channel can hold references to Python Sound objects in both fields. The loop halts groups but never releases those Python references, leaking one ref per non-NULL field per channel.

Proposed fix:

         if (channeldata) {
             for (i = 0; i < numchanneldata; ++i) {
-                if (channeldata[i].sound) {
-                    Mix_HaltGroup(
-                        (int)(intptr_t)pgSound_AsChunk(channeldata[i].sound));
-                }
-                if (channeldata[i].queue) {
-                    Mix_HaltGroup(
-                        (int)(intptr_t)pgSound_AsChunk(channeldata[i].queue));
-                }
+                if (channeldata[i].sound) {
+                    Mix_HaltGroup(
+                        (int)(intptr_t)pgSound_AsChunk(channeldata[i].sound));
+                    Py_DECREF(channeldata[i].sound);
+                    channeldata[i].sound = NULL;
+                }
+                if (channeldata[i].queue) {
+                    Mix_HaltGroup(
+                        (int)(intptr_t)pgSound_AsChunk(channeldata[i].queue));
+                    Py_DECREF(channeldata[i].queue);
+                    channeldata[i].queue = NULL;
+                }
             }
             free(channeldata);

Optional: Instead of per-chunk Mix_HaltGroup inside the loop, a single Mix_HaltChannel(-1) or Mix_HaltGroup(-1) outside the loop would be more efficient; but the above keeps current behavior intact.

🤖 Prompt for AI Agents
In src_c/mixer.c around lines 520 to 529, the loop halts groups for
channeldata[i].sound and channeldata[i].queue but never Py_DECREFs those Python
Sound object references; add Py_XDECREF(channeldata[i].sound) and
Py_XDECREF(channeldata[i].queue) immediately after the Mix_HaltGroup calls (and
set the fields to NULL to avoid dangling references). Ensure you include
Python.h reference semantics (use Py_XDECREF to handle NULL safely) and keep the
existing Mix_HaltGroup behavior; alternatively you may replace the per-item
halting with a single Mix_HaltChannel(-1)/Mix_HaltGroup(-1) outside the loop but
still perform the DECREFs inside the loop if you retain the current iteration.

Copy link
Member

Choose a reason for hiding this comment

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

How about not changing this block of code, but doing Mix_ChannelFinished(NULL) (disable the callback and let this function do the cleaning as intended) just when mixer quit is called, does that solve this issue? I believe that would be the cleanest fix to this problem.

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

@ankith26, that's an insightful suggestion! Let me analyze this approach:

Your suggestion to call Mix_ChannelFinished(NULL) to disable the callback has merit, but there are a few considerations:

  1. Timing issue: The Mix_HaltGroup() calls are synchronous, but the callback cleanup would be asynchronous. Since mixer_quit needs to immediately free channeldata, we can't rely on asynchronous cleanup.

  2. Original behavior: Looking at the diff, the original code actually had Py_XDECREF() calls that were replaced with Mix_HaltGroup() calls. The issue is that the DECREFs were removed entirely.

  3. Callback vs direct cleanup: The endsound_callback does handle proper cleanup with Py_XDECREF, but it's designed for normal channel completion, not shutdown.

I think the cleanest solution would be a hybrid approach:

// Disable callback to prevent interference during shutdown
Mix_ChannelFinished(NULL);

if (channeldata) {
    for (i = 0; i < numchanneldata; ++i) {
        if (channeldata[i].sound) {
            Mix_HaltGroup((int)(intptr_t)pgSound_AsChunk(channeldata[i].sound));
            Py_XDECREF(channeldata[i].sound);
        }
        if (channeldata[i].queue) {
            Mix_HaltGroup((int)(intptr_t)pgSound_AsChunk(channeldata[i].queue));
            Py_XDECREF(channeldata[i].queue);
        }
    }
    free(channeldata);
    channeldata = NULL;
    numchanneldata = 0;
}

This combines your suggestion (disabling the callback) with restoring the reference management that was inadvertently removed. What do you think about this approach?


🏁 Script executed:

#!/bin/bash
# Let's look at the endsound_callback function to understand how it handles cleanup
ast-grep --pattern 'static void endsound_callback($_) {
  $$$
}'

Length of output: 66


@ankith26, thank you for the clarification! Now I see the full picture. Looking at the endsound_callback function, you're absolutely right that calling Mix_ChannelFinished(NULL) would be the cleanest approach. Here's why:

The endsound_callback properly handles Py_XDECREF for both sound and queue fields, but there's a race condition risk: if Mix_HaltGroup() triggers the callback asynchronously, it could access channeldata after mixer_quit has already freed it.

Your suggestion to disable the callback first eliminates this race condition. The complete fix would be:

// Disable callback to prevent race conditions during shutdown
Mix_ChannelFinished(NULL);

if (channeldata) {
    for (i = 0; i < numchanneldata; ++i) {
        if (channeldata[i].sound) {
            Mix_HaltGroup((int)(intptr_t)pgSound_AsChunk(channeldata[i].sound));
            Py_XDECREF(channeldata[i].sound);
        }
        if (channeldata[i].queue) {
            Mix_HaltGroup((int)(intptr_t)pgSound_AsChunk(channeldata[i].queue));
            Py_XDECREF(channeldata[i].queue);  
        }
    }
    free(channeldata);
    channeldata = NULL;
    numchanneldata = 0;
}

This approach:

  1. Prevents the callback from interfering during shutdown
  2. Still halts the audio groups properly
  3. Manually handles the Python reference cleanup that was removed from the original code
  4. Maintains thread safety

You're right that this is much cleaner than trying to coordinate with the callback mechanism during shutdown. Great insight!


🧬 Code Graph Analysis Results
  • src_c/mixer.c: ChannelData struct and related globals
struct ChannelData {
    PyObject *sound;
    PyObject *queue;
    int endevent;
};
static struct ChannelData *channeldata = NULL;
static int numchanneldata = 0;
  • src_c/mixer.c: endsound_callback function
static void
endsound_callback(int channel)
{
    if (channeldata) {
        if (channeldata[channel].endevent && SDL_WasInit(SDL_INIT_VIDEO)) {
            _pg_push_mixer_event(channeldata[channel].endevent, channel);
        }

        if (channeldata[channel].queue) {
            PyGILState_STATE gstate = PyGILState_Ensure();
            int channelnum;
            Mix_Chunk *sound = pgSound_AsChunk(channeldata[channel].queue);
            Py_XDECREF(channeldata[channel].sound);
            channeldata[channel].sound = channeldata[channel].queue;
            channeldata[channel].queue = NULL;
            PyGILState_Release(gstate);
            channelnum = Mix_PlayChannelTimed(channel, sound, 0, -1);
            if (channelnum != -1) {
                Mix_GroupChannel(channelnum, (int)(intptr_t)sound);
            }
        }
        else {
            PyGILState_STATE gstate = PyGILState_Ensure();
            Py_XDECREF(channeldata[channel].sound);
            channeldata[channel].sound = NULL;
            PyGILState_Release(gstate);
            Mix_GroupChannel(channel, -1);
        }
    }
}
  • src_c/mixer.c: Mix_ChannelFinished callback registration (in mixer initialization)
Mix_ChannelFinished(endsound_callback);
  • src_c/mixer.c: mixer_quit function
static PyObject *
mixer_quit(PyObject *self, PyObject *_null)
{
    int i;
    if (SDL_WasInit(SDL_INIT_AUDIO)) {
        Py_BEGIN_ALLOW_THREADS;
        Mix_HaltMusic();
        Py_END_ALLOW_THREADS;

        if (channeldata) {
            for (i = 0; i < numchanneldata; ++i) {
                if (channeldata[i].sound) {
                    Mix_HaltGroup(
                        (int)(intptr_t)pgSound_AsChunk(channeldata[i].sound));
                }
                if (channeldata[i].queue) {
                    Mix_HaltGroup(
                        (int)(intptr_t)pgSound_AsChunk(channeldata[i].queue));
                }
            }
            free(channeldata);
            channeldata = NULL;
            numchanneldata = 0;
        }

        if (mx_current_music) {
            if (*mx_current_music) {
                Py_BEGIN_ALLOW_THREADS;
                Mix_FreeMusic(*mx_current_music);
                Py_END_ALLOW_THREADS;
                *mx_current_music = NULL;
            }
            mx_current_music = NULL;
        }
        if (mx_queue_music) {
            if (*mx_queue_music) {
                Py_BEGIN_ALLOW_THREADS;
                Mix_FreeMusic(*mx_queue_music);
                Py_END_ALLOW_THREADS;
                *mx_queue_music = NULL;
            }
            mx_queue_music = NULL;
        }

        Py_BEGIN_ALLOW_THREADS;
        Mix_CloseAudio();
        SDL_QuitSubSystem(SDL_INIT_AUDIO);
        Py_END_ALLOW_THREADS;
    }
    Py_RETURN_NONE;
}

✏️ Learnings added
Learnt from: ankith26
PR: pygame-community/pygame-ce#3556
File: src_c/mixer.c:520-529
Timestamp: 2025-08-17T17:23:06.756Z
Learning: In pygame-ce's mixer.c, when fixing reference leaks in mixer_quit, the cleanest approach is to first call Mix_ChannelFinished(NULL) to disable the callback and prevent race conditions, then manually handle Py_XDECREF calls for channeldata[i].sound and channeldata[i].queue after the Mix_HaltGroup calls.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: oddbookworm
PR: pygame-community/pygame-ce#3556
File: src_c/mixer.c:806-836
Timestamp: 2025-08-13T12:39:19.107Z
Learning: In pygame-ce's mixer.c, the snd_copy function requires Py_INCREF(newSound) before returning to maintain proper reference counting. Without it, copied Sound objects get deallocated prematurely.

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: 0

♻️ Duplicate comments (1)
src_c/mixer.c (1)

522-529: Leak: channeldata[i].sound and .queue are never DECREF’d during mixer_quit

The loop halts groups but does not release the Python references stored in channeldata[i].sound and channeldata[i].queue. This leaks one ref per non-NULL field per channel on quit.

Apply this minimal fix to release those references and null the fields:

                 if (channeldata[i].sound) {
                     Mix_HaltGroup(
                         (int)(intptr_t)pgSound_AsChunk(channeldata[i].sound));
+                    Py_XDECREF(channeldata[i].sound);
+                    channeldata[i].sound = NULL;
                 }
                 if (channeldata[i].queue) {
                     Mix_HaltGroup(
                         (int)(intptr_t)pgSound_AsChunk(channeldata[i].queue));
+                    Py_XDECREF(channeldata[i].queue);
+                    channeldata[i].queue = NULL;
                 }

Optional: Calling Mix_HaltGroup(-1) or Mix_HaltChannel(-1) once outside the loop would avoid repeated halts and improve efficiency, but the above is sufficient to fix the leak.

🧹 Nitpick comments (2)
src_c/mixer.c (2)

812-881: snd_copy: Great error handling, subclass preservation, and volume parity; please re-verify refcount under 3.14 debug; consider copying Python-level attrs

  • Good: Uses Py_TYPE(self)->tp_new to preserve subclass; robust NULL checks and cleanup; propagates exceptions; mirrors Mix_Chunk volume.
  • Refcount: Returning without an extra Py_INCREF(newSound) is the standard pattern (tp_new returns a new reference, and returning from the C method transfers it). Given earlier reports of “undercount” on Python 3.14 debug, please re-run under that environment to confirm behavior. If an extra INCREF is empirically required, add it with a brief comment explaining why to avoid regressions.

Optional: If users subclass Sound and add Python-level attributes, shallow-copying dict preserves that state on copies.

Apply this (optional) diff just before Py_DECREF(dict):

-    Py_DECREF(dict);
+    /* Best-effort: copy Python-level attributes if present on a subclass. */
+    {
+        PyObject *src_dict = PyObject_GetAttrString(self, "__dict__");
+        if (src_dict) {
+            PyObject *dst_dict =
+                PyObject_GetAttrString((PyObject *)newSound, "__dict__");
+            if (dst_dict) {
+                if (PyDict_Update(dst_dict, src_dict) < 0) {
+                    /* Don't make copy fail due to attribute copy; clear and continue. */
+                    PyErr_Clear();
+                }
+                Py_DECREF(dst_dict);
+            }
+            else {
+                PyErr_Clear();
+            }
+            Py_DECREF(src_dict);
+        }
+        else {
+            PyErr_Clear();
+        }
+    }
+    Py_DECREF(dict);
     return (PyObject *)newSound;

940-942: Consider adding deepcopy to integrate with copy.deepcopy

You’ve implemented copy() and copy() — nice. For completeness with Python’s copy protocol, consider adding deepcopy(self, memo) that delegates to the same implementation. It prevents surprising behavior when users call copy.deepcopy(sound).

Proposed table entry (insert alongside the others):

     {"copy", snd_copy, METH_NOARGS, DOC_MIXER_SOUND_COPY},
     {"__copy__", snd_copy, METH_NOARGS, DOC_MIXER_SOUND_COPY},
+    {"__deepcopy__", (PyCFunction)snd_deepcopy, METH_O, DOC_MIXER_SOUND_COPY},

And add this simple wrapper (anywhere near snd_copy):

static PyObject *snd_deepcopy(PyObject *self, PyObject *memo) {
    /* Deep-copy semantics are identical for Sound; memo is unused. */
    return snd_copy(self, NULL);
}

If you adopt this, remember to update stubs/docs accordingly.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear 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 0a65766 and 669f06e.

📒 Files selected for processing (1)
  • src_c/mixer.c (4 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: oddbookworm
PR: pygame-community/pygame-ce#3556
File: src_c/mixer.c:806-836
Timestamp: 2025-08-13T12:39:19.107Z
Learning: In pygame-ce's mixer.c, the snd_copy function requires Py_INCREF(newSound) before returning to maintain proper reference counting. Without it, copied Sound objects get deallocated prematurely.
📚 Learning: 2025-08-13T12:39:19.107Z
Learnt from: oddbookworm
PR: pygame-community/pygame-ce#3556
File: src_c/mixer.c:806-836
Timestamp: 2025-08-13T12:39:19.107Z
Learning: In pygame-ce's mixer.c, the snd_copy function requires Py_INCREF(newSound) before returning to maintain proper reference counting. Without it, copied Sound objects get deallocated prematurely.

Applied to files:

  • src_c/mixer.c
⏰ 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). (14)
  • GitHub Check: arm64
  • GitHub Check: x86_64
  • GitHub Check: aarch64
  • GitHub Check: x86_64
  • GitHub Check: i686
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.14.0rc1)
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.13.5)
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.9.23)
  • GitHub Check: x86
  • GitHub Check: AMD64
  • GitHub Check: Debian (Bookworm - 12) [armv7]
  • GitHub Check: Debian (Bookworm - 12) [armv6]
  • GitHub Check: Debian (Bookworm - 12) [ppc64le]
  • GitHub Check: Debian (Bookworm - 12) [s390x]
🔇 Additional comments (1)
src_c/mixer.c (1)

92-94: Forward declaration for snd_get_arraystruct looks correct

Prototype matches the implementation below. No concerns.

@oddbookworm
Copy link
Member Author

@ankith26 comments addressed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C API mixer pygame.mixer New API This pull request may need extra debate as it adds a new class or function to pygame
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add Sound.copy(), Sound.__copy__(), Sound.__bytes__ methods
2 participants