Skip to content

Conversation

@Starbuck5
Copy link
Member

@Starbuck5 Starbuck5 commented Dec 9, 2025

For #3581 mega-issue.

To fully implement next generation mixer, I realized I needed parts of SDL3's audio subsystem to be exposed. Not for a ton of API surface, but it is necessary for full implementation and necessary for a half-decent compatibility layer with the old mixer API. And it will need to be written anyway for full SDL3 support.

So I spent some time and wrote a whole new module, SDL3 only, starting from the type stubs mocked up in #3581.

I experimented with a hybrid approach with C level PyObjects as well as Python level classes. I'm not sure I'm in love with the exact way I did it, but I think it definitely shows promise as an approach. Some of the classes are only in Python, some are wrappers over low-level calls and "state objects". I really like how convenient it is to add error checking or stuff like the AudioDevice weakref dict at the Python level.

Example script:

import pygame
from pygame import _audio as audio
pygame.audio = audio

WAV_PATH = r"C:\Users\charl\Desktop\pygame-ce\examples\data\house_lo.wav"

audio.init()

print("==AUDIO METADATA==")
print("Available drivers:", pygame.audio.get_drivers())
print("Current driver:", pygame.audio.get_current_driver())

print("Playback devices--")

devices = pygame.audio.get_playback_devices()
for device in devices:
    print(device.name)

print("==PLAYING AUDIO==")

print("Opening default playback device--")
logical_device = pygame.audio.DEFAULT_PLAYBACK_DEVICE.open()
print(logical_device.name)

print("Loading audio--")
audio_spec, audio_bytes = pygame.audio.load_wav(WAV_PATH)
print(audio_spec, "|", len(audio_bytes), "bytes")

print("Creating stream")
stream = pygame.audio.AudioStream(audio_spec, audio_spec)
print(stream)
stream.frequency_ratio = 1.4
stream.gain = 0.4
stream.bind(logical_device)
stream.put_data(audio_bytes)
stream.flush()

# It can change after binding to a device
print("Stream output spec now:", stream.dst_spec)

# Wait for stream to be exhausted
while stream.num_queued_bytes:
    pygame.time.wait(100)

print("Done.")

Next steps (future PRs):

  • Docs
  • Tests
  • Complete implementation (quit support if we want it, callbacks, other unimplemented API)
  • Next gen mixer
  • Old mixer compat layer on next gen mixer

@Starbuck5 Starbuck5 requested a review from a team as a code owner December 9, 2025 08:25
@Starbuck5 Starbuck5 added New API This pull request may need extra debate as it adds a new class or function to pygame mixer pygame.mixer sdl3 labels Dec 9, 2025
@Starbuck5 Starbuck5 marked this pull request as draft December 9, 2025 08:25
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 9, 2025

📝 Walkthrough

Walkthrough

Adds an SDL3-targeted audio subsystem: new type stubs, a C extension exposing device/stream state, a Python high-level wrapper, and Meson build updates to include the module and adjust mixer targets for SDL3.

Changes

Cohort / File(s) Summary
Type stubs
buildconfig/stubs/pygame/_audio.pyi
New stub defining AudioFormat, AudioSpec, AudioDevice, LogicalAudioDevice, AudioStream, module functions (init, get_init, get_current_driver, get_drivers, get_playback_devices, get_recording_devices, load_wav), and DEFAULT_* device constants.
C backend
src_c/_base_audio.c
New C extension implementing SDL3 audio bindings: heap types AudioDeviceState and AudioStreamState, module functions (init/quit/get_init/get_current_driver/get_drivers/load_wav/get_playback/get_recording/get_default device), stream/device lifecycle and operations, GC traverse/clear, error handling, and module init wiring.
Python API
src_py/_audio.py
New Python wrapper exposing immutable AudioFormat/AudioSpec, AudioDevice/LogicalAudioDevice, AudioStream, AudioInternals helpers, public functions/constants mapping to the C backend, and input validation/error handling.
C build config
src_c/meson.build
Adds _base_audio extension target (built from _base_audio.c) for SDL3 and re-scopes mixer/mixer_music targets to be skipped when sdl_api == 3.
Python build config
src_py/meson.build
Installs src_py/_audio.py conditionally when sdl_api == 3.
Stub packaging & allowlist
buildconfig/stubs/meson.build, buildconfig/stubs/mypy_allow_list.txt
Adjusts stub exclusion list to omit _audio.pyi only when appropriate and adds pygame._audio to the mypy allowlist for the SDL3 transition.

Sequence Diagram(s)

sequenceDiagram
    actor App as Application
    participant PyAPI as Python Audio API (_audio.py)
    participant CExt as C Backend (_base_audio.c)
    participant SDL3 as SDL3 Audio Subsystem

    Note over App,SDL3: Device discovery & opening

    App->>PyAPI: get_playback_devices()
    PyAPI->>CExt: pg_audio_get_playback_device_states()
    CExt->>SDL3: query output devices
    SDL3-->>CExt: device list
    CExt-->>PyAPI: device state list
    PyAPI-->>App: AudioDevice[]

    App->>PyAPI: device.open(spec)
    PyAPI->>CExt: open device with AudioSpec
    CExt->>SDL3: open device handle
    SDL3-->>CExt: device handle
    CExt-->>PyAPI: LogicalAudioDevice
    PyAPI-->>App: LogicalAudioDevice
Loading
sequenceDiagram
    actor App as Application
    participant PyAPI as Python Audio API (_audio.py)
    participant CExt as C Backend (_base_audio.c)
    participant SDL3 as SDL3 Audio Subsystem

    Note over App,SDL3: Stream lifecycle & data flow

    App->>PyAPI: AudioStream(src_spec, dst_spec)
    PyAPI->>CExt: create AudioStreamState
    CExt->>SDL3: create audio stream
    SDL3-->>CExt: stream handle
    CExt-->>PyAPI: AudioStream

    App->>PyAPI: stream.bind(device)
    PyAPI->>CExt: bind stream to device
    CExt->>SDL3: bind stream to device handle
    SDL3-->>CExt: result
    CExt-->>PyAPI: bound / error

    App->>PyAPI: stream.put_data(bytes)
    PyAPI->>CExt: queue data
    CExt->>SDL3: feed stream data
    SDL3-->>CExt: queued bytes
    CExt-->>PyAPI: ack
    PyAPI-->>App: queued bytes
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Pay attention to:
    • src_c/_base_audio.c: SDL3 API usage, memory/refcounting, GC traverse/clear, error propagation.
    • src_py/_audio.py: spec binding rules, gain/frequency validation, and mapping to C backend.
    • Meson changes: conditional inclusion/exclusion to ensure correct builds for SDL2 vs SDL3.

Possibly related issues

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.53% 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
Title check ✅ Passed The title accurately reflects the main change: adding a new SDL3 audio module (_audio) as a new public API surface.
Description check ✅ Passed The description is directly related to the changeset, explaining the motivation, implementation approach, example usage, and future steps for the SDL3 audio module.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6e18b27 and 374f237.

📒 Files selected for processing (7)
  • buildconfig/stubs/meson.build (1 hunks)
  • buildconfig/stubs/mypy_allow_list.txt (1 hunks)
  • buildconfig/stubs/pygame/_audio.pyi (1 hunks)
  • src_c/_base_audio.c (1 hunks)
  • src_c/meson.build (1 hunks)
  • src_py/_audio.py (1 hunks)
  • src_py/meson.build (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • buildconfig/stubs/pygame/_audio.pyi
🧰 Additional context used
🧠 Learnings (7)
📚 Learning: 2025-09-01T20:18:57.500Z
Learnt from: Starbuck5
Repo: pygame-community/pygame-ce PR: 3573
File: src_c/_pygame.h:108-133
Timestamp: 2025-09-01T20:18:57.500Z
Learning: In pygame-ce PR #3573, PG_SURF_BitsPerPixel was changed from a simple macro to a static inline function to handle SDL2/SDL3 differences. SDL2's BitsPerPixel includes padding bits (e.g., RGBX => 32) while SDL3's SDL_BITSPERPIXEL excludes padding (e.g., RGBX => 24). The new implementation bridges this gap rather than removing the helper entirely.

Applied to files:

  • src_py/meson.build
  • src_c/meson.build
  • buildconfig/stubs/mypy_allow_list.txt
  • src_c/_base_audio.c
📚 Learning: 2025-10-17T08:53:32.449Z
Learnt from: damusss
Repo: pygame-community/pygame-ce PR: 3219
File: src_c/window.c:11-13
Timestamp: 2025-10-17T08:53:32.449Z
Learning: SDL3's first stable release is version 3.2.0 (released January 21, 2025). SDL 3.0.x and 3.1.x were preview/pre-release versions not intended for production use, so version checks for SDL3 features in pygame-ce should use SDL_VERSION_ATLEAST(3, 2, 0) to target the first stable release.

Applied to files:

  • buildconfig/stubs/mypy_allow_list.txt
📚 Learning: 2025-09-01T20:22:31.010Z
Learnt from: Starbuck5
Repo: pygame-community/pygame-ce PR: 3573
File: src_c/_pygame.h:115-126
Timestamp: 2025-09-01T20:22:31.010Z
Learning: In pygame-ce PR #3573, the PG_SURF_BitsPerPixel function implementation uses hardcoded return values of 32 for YUY2/UYVY/YVYU FOURCC formats because it's copying the exact logic from SDL's internal SDL_GetMasksForPixelFormat function. This ensures perfect compatibility between SDL2 and SDL3 behaviors rather than using the technically more accurate values that might be returned by SDL_GetPixelFormatDetails.

Applied to files:

  • buildconfig/stubs/mypy_allow_list.txt
📚 Learning: 2025-08-17T17:23:06.787Z
Learnt from: ankith26
Repo: pygame-community/pygame-ce PR: 3556
File: src_c/mixer.c:520-529
Timestamp: 2025-08-17T17:23:06.787Z
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.

Applied to files:

  • buildconfig/stubs/mypy_allow_list.txt
  • src_c/_base_audio.c
📚 Learning: 2025-10-08T08:15:01.411Z
Learnt from: ankith26
Repo: pygame-community/pygame-ce PR: 3602
File: buildconfig/stubs/pygame/locals.pyi:601-1198
Timestamp: 2025-10-08T08:15:01.411Z
Learning: The repository pygame-community/pygame-ce is for pygame-ce (pygame community edition), not the original pygame project. Always refer to it as pygame-ce in reviews and analyses.

Applied to files:

  • buildconfig/stubs/mypy_allow_list.txt
📚 Learning: 2025-08-13T12:39:19.130Z
Learnt from: oddbookworm
Repo: pygame-community/pygame-ce PR: 3556
File: src_c/mixer.c:806-836
Timestamp: 2025-08-13T12:39:19.130Z
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/_base_audio.c
📚 Learning: 2025-08-30T21:11:00.240Z
Learnt from: Starbuck5
Repo: pygame-community/pygame-ce PR: 3573
File: src_c/_camera.c:129-131
Timestamp: 2025-08-30T21:11:00.240Z
Learning: When doing SDL2 to SDL3 compatibility changes, Starbuck5 prefers to maintain exact existing behaviors even when other improvements could be made, focusing solely on the compatibility aspect rather than mixing in behavioral fixes.

Applied to files:

  • src_c/_base_audio.c
🧬 Code graph analysis (2)
src_c/_base_audio.c (1)
src_c/base.c (2)
  • import_pygame_base (2294-2297)
  • import_pygame_rwobject (2343-2346)
src_py/_audio.py (2)
buildconfig/stubs/pygame/_audio.pyi (57)
  • AudioFormat (34-56)
  • bitsize (36-36)
  • bytesize (38-38)
  • is_float (40-40)
  • is_int (42-42)
  • is_big_endian (44-44)
  • is_little_endian (46-46)
  • is_signed (48-48)
  • is_unsigned (50-50)
  • name (52-52)
  • name (94-94)
  • silence_value (54-54)
  • AudioSpec (71-81)
  • format (74-74)
  • channels (76-76)
  • frequency (78-78)
  • framesize (80-80)
  • AudioDevice (83-97)
  • open (84-84)
  • device (135-135)
  • LogicalAudioDevice (99-116)
  • is_playback (92-92)
  • channel_map (97-97)
  • pause (100-100)
  • resume (101-101)
  • paused (103-103)
  • gain (105-105)
  • gain (107-107)
  • gain (145-145)
  • gain (147-147)
  • AudioStream (118-164)
  • src_spec (137-137)
  • src_spec (139-139)
  • dst_spec (141-141)
  • dst_spec (143-143)
  • bind (120-120)
  • unbind (121-121)
  • clear (122-122)
  • flush (123-123)
  • num_available_bytes (125-125)
  • num_queued_bytes (127-127)
  • get_data (128-128)
  • put_data (129-129)
  • pause_device (130-130)
  • resume_device (131-131)
  • device_paused (133-133)
  • frequency_ratio (149-149)
  • frequency_ratio (151-151)
  • lock (156-156)
  • unlock (157-157)
  • init (10-10)
  • get_init (13-13)
  • get_current_driver (14-14)
  • get_drivers (15-15)
  • get_playback_devices (16-16)
  • get_recording_devices (17-17)
  • load_wav (20-20)
buildconfig/stubs/pygame/base.pyi (1)
  • get_sdl_byteorder (18-18)
🪛 Flake8 (7.3.0)
src_py/_audio.py

[error] 339-339: undefined name 'weakref'

(F821)

🪛 Ruff (0.14.8)
src_py/_audio.py

79-81: Avoid specifying long messages outside the exception class

(TRY003)


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

(TRY003)


128-131: Avoid specifying long messages outside the exception class

(TRY003)


174-176: Avoid specifying long messages outside the exception class

(TRY003)


178-180: Avoid specifying long messages outside the exception class

(TRY003)


194-196: Avoid specifying long messages outside the exception class

(TRY003)


250-252: Avoid specifying long messages outside the exception class

(TRY003)


258-260: Avoid specifying long messages outside the exception class

(TRY003)


282-284: Avoid specifying long messages outside the exception class

(TRY003)


290-292: Avoid specifying long messages outside the exception class

(TRY003)


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

(TRY003)


316-318: Avoid specifying long messages outside the exception class

(TRY003)


339-339: Undefined name weakref

(F821)


353-356: Avoid specifying long messages outside the exception class

(TRY003)

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

31-33: LGTM!

The conditional installation of _audio.py is correctly gated for SDL3-only builds, aligning with the module's SDL3 dependency.

buildconfig/stubs/mypy_allow_list.txt (1)

30-32: LGTM!

The temporary exclusion is appropriate for the new SDL3 audio module. The comment ensures this will be revisited when SDL3 stub checking is enabled.

buildconfig/stubs/meson.build (1)

1-10: LGTM!

The dynamic exclusion logic correctly ensures _audio.pyi is only installed for SDL3 builds. The conditional logic is properly inverted (exclude when sdl_api != 3).

src_c/meson.build (2)

423-433: LGTM!

The _base_audio extension module is correctly configured for SDL3-only builds, following the established pattern for other pygame-ce modules.


435-456: LGTM!

The conditional nesting correctly disables mixer modules for SDL3 builds while preserving the existing SDL2 behavior. The TODO comment appropriately indicates future work.

src_py/_audio.py (1)

1-444: LGTM!

The Python audio API wrapper is well-structured with:

  • Clean separation between Python convenience layer and C backend
  • Appropriate use of immutable design for AudioFormat and AudioSpec
  • Good defensive programming with type checking and validation
  • Proper resource management with weakref for device tracking

Past review comments appear to have been addressed. The static analysis warnings are either false positives (undefined weakref) or pedantic style suggestions (TRY003) that don't affect functionality.

src_c/_base_audio.c (1)

1-1137: LGTM!

The C implementation of the SDL3 audio backend is well-structured and all critical issues from previous reviews have been addressed:

✅ Resource leak fixes for allocation failures (device/stream creation)
✅ Correct type handling (Py_ssize_t for PyBytes_AsStringAndSize)
✅ Reference counting fixes (Py_BuildValue with "N" format, proper DECREF on success paths)
✅ NULL checks for all allocation functions (PyList_New, tp_alloc, PyType_FromModuleAndSpec)
✅ Proper cleanup on error paths (SDL resource cleanup before returning NULL)
✅ SDL3-specific handling (physical vs logical device IDs)

The module follows pygame-ce patterns with proper GC support, module state management, and multi-phase initialization.


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

❤️ Share

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

Copy link
Contributor

@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: 14

🧹 Nitpick comments (4)
src_c/_base_audio.c (1)

57-61: Potential type mismatch for id member.

SDL_AudioDeviceID is defined as Uint32 (unsigned 32-bit), but Py_T_INT is a signed int. This could cause issues if SDL returns device IDs with the high bit set, as they would appear as negative values in Python.

Consider using Py_T_UINT instead:

 static PyMemberDef adevice_state_members[] = {
-    {"id", Py_T_INT, offsetof(PGAudioDeviceStateObject, devid), Py_READONLY,
+    {"id", Py_T_UINT, offsetof(PGAudioDeviceStateObject, devid), Py_READONLY,
      NULL},
     {NULL} /* Sentinel */
 };
buildconfig/stubs/pygame/_audio.pyi (1)

1-2: Unused imports.

Callable and TypeVar are imported but currently unused since the callback-related type aliases (lines 26-29) are commented out.

Consider removing these imports until the callback functionality is implemented:

-from collections.abc import Callable
-from typing import TypeVar
-
 from pygame.typing import FileLike
src_py/_audio.py (2)

22-22: Consider ClassVar annotation for mutable class attribute.

The format_num_to_instance dict is intentionally class-level and shared. Adding ClassVar clarifies intent and satisfies type checkers.

+from typing import ClassVar
+
 class AudioFormat:
     # ...
-    format_num_to_instance = dict()
+    format_num_to_instance: ClassVar[dict[int, "AudioFormat"]] = {}

7-10: Acknowledged: TODO items for documentation, tests, and quit safety.

The noted concerns about making quit() safe are valid - SDL objects become invalid after subsystem quit. Consider implementing reference tracking or invalidation callbacks before exposing quit() publicly.

Would you like me to help design a pattern for safely invalidating Python wrapper objects when the audio subsystem is quit?

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6f3b01a and 465a2fc.

📒 Files selected for processing (5)
  • buildconfig/stubs/pygame/_audio.pyi (1 hunks)
  • src_c/_base_audio.c (1 hunks)
  • src_c/meson.build (1 hunks)
  • src_py/_audio.py (1 hunks)
  • src_py/meson.build (1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: Starbuck5
Repo: pygame-community/pygame-ce PR: 3573
File: src_c/_camera.c:129-131
Timestamp: 2025-08-30T21:11:00.240Z
Learning: When doing SDL2 to SDL3 compatibility changes, Starbuck5 prefers to maintain exact existing behaviors even when other improvements could be made, focusing solely on the compatibility aspect rather than mixing in behavioral fixes.
Learnt from: damusss
Repo: pygame-community/pygame-ce PR: 3219
File: src_c/window.c:11-13
Timestamp: 2025-10-17T08:53:32.449Z
Learning: SDL3's first stable release is version 3.2.0 (released January 21, 2025). SDL 3.0.x and 3.1.x were preview/pre-release versions not intended for production use, so version checks for SDL3 features in pygame-ce should use SDL_VERSION_ATLEAST(3, 2, 0) to target the first stable release.
Learnt from: Starbuck5
Repo: pygame-community/pygame-ce PR: 3573
File: src_c/_pygame.h:108-133
Timestamp: 2025-09-01T20:18:57.500Z
Learning: In pygame-ce PR #3573, PG_SURF_BitsPerPixel was changed from a simple macro to a static inline function to handle SDL2/SDL3 differences. SDL2's BitsPerPixel includes padding bits (e.g., RGBX => 32) while SDL3's SDL_BITSPERPIXEL excludes padding (e.g., RGBX => 24). The new implementation bridges this gap rather than removing the helper entirely.
Learnt from: ankith26
Repo: pygame-community/pygame-ce PR: 3556
File: src_c/mixer.c:520-529
Timestamp: 2025-08-17T17:23:06.787Z
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.
Learnt from: oddbookworm
Repo: pygame-community/pygame-ce PR: 3556
File: src_c/mixer.c:806-836
Timestamp: 2025-08-13T12:39:19.130Z
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.
Learnt from: Starbuck5
Repo: pygame-community/pygame-ce PR: 3573
File: src_c/_pygame.h:115-126
Timestamp: 2025-09-01T20:22:31.010Z
Learning: In pygame-ce PR #3573, the PG_SURF_BitsPerPixel function implementation uses hardcoded return values of 32 for YUY2/UYVY/YVYU FOURCC formats because it's copying the exact logic from SDL's internal SDL_GetMasksForPixelFormat function. This ensures perfect compatibility between SDL2 and SDL3 behaviors rather than using the technically more accurate values that might be returned by SDL_GetPixelFormatDetails.
📚 Learning: 2025-09-01T20:18:57.500Z
Learnt from: Starbuck5
Repo: pygame-community/pygame-ce PR: 3573
File: src_c/_pygame.h:108-133
Timestamp: 2025-09-01T20:18:57.500Z
Learning: In pygame-ce PR #3573, PG_SURF_BitsPerPixel was changed from a simple macro to a static inline function to handle SDL2/SDL3 differences. SDL2's BitsPerPixel includes padding bits (e.g., RGBX => 32) while SDL3's SDL_BITSPERPIXEL excludes padding (e.g., RGBX => 24). The new implementation bridges this gap rather than removing the helper entirely.

Applied to files:

  • src_py/meson.build
  • src_c/meson.build
📚 Learning: 2025-10-17T08:53:32.449Z
Learnt from: damusss
Repo: pygame-community/pygame-ce PR: 3219
File: src_c/window.c:11-13
Timestamp: 2025-10-17T08:53:32.449Z
Learning: SDL3's first stable release is version 3.2.0 (released January 21, 2025). SDL 3.0.x and 3.1.x were preview/pre-release versions not intended for production use, so version checks for SDL3 features in pygame-ce should use SDL_VERSION_ATLEAST(3, 2, 0) to target the first stable release.

Applied to files:

  • src_py/meson.build
🧬 Code graph analysis (2)
src_c/_base_audio.c (1)
src_c/base.c (2)
  • import_pygame_base (2294-2297)
  • import_pygame_rwobject (2343-2346)
buildconfig/stubs/pygame/_audio.pyi (1)
src_py/_audio.py (57)
  • init (358-359)
  • get_init (367-368)
  • get_current_driver (371-372)
  • get_drivers (375-376)
  • get_playback_devices (402-406)
  • AudioDevice (139-172)
  • get_recording_devices (409-413)
  • load_wav (416-421)
  • AudioSpec (99-136)
  • AudioFormat (13-76)
  • bitsize (31-32)
  • bytesize (35-36)
  • is_float (39-40)
  • is_int (43-44)
  • is_big_endian (47-48)
  • is_little_endian (51-52)
  • is_signed (55-56)
  • is_unsigned (59-60)
  • name (63-64)
  • name (167-168)
  • silence_value (67-68)
  • format (117-118)
  • channels (121-122)
  • frequency (125-126)
  • framesize (129-130)
  • open (145-159)
  • LogicalAudioDevice (175-192)
  • is_playback (162-163)
  • channel_map (171-172)
  • pause (176-177)
  • resume (179-180)
  • paused (183-184)
  • gain (187-188)
  • gain (191-192)
  • gain (323-324)
  • gain (327-331)
  • AudioStream (195-355)
  • src_spec (266-269)
  • src_spec (272-288)
  • dst_spec (291-301)
  • dst_spec (304-320)
  • bind (216-223)
  • device (262-263)
  • unbind (225-227)
  • clear (229-230)
  • flush (232-233)
  • num_available_bytes (236-237)
  • num_queued_bytes (240-241)
  • get_data (243-244)
  • put_data (248-249)
  • pause_device (251-252)
  • resume_device (254-255)
  • device_paused (258-259)
  • frequency_ratio (334-335)
  • frequency_ratio (338-342)
  • lock (344-345)
  • unlock (347-348)
🪛 Ruff (0.14.8)
src_py/_audio.py

22-22: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


102-104: Avoid specifying long messages outside the exception class

(TRY003)


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

(TRY003)


153-155: Avoid specifying long messages outside the exception class

(TRY003)


198-200: Avoid specifying long messages outside the exception class

(TRY003)


202-204: Avoid specifying long messages outside the exception class

(TRY003)


218-220: Avoid specifying long messages outside the exception class

(TRY003)


274-276: Avoid specifying long messages outside the exception class

(TRY003)


282-284: Avoid specifying long messages outside the exception class

(TRY003)


306-308: Avoid specifying long messages outside the exception class

(TRY003)


314-316: Avoid specifying long messages outside the exception class

(TRY003)


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

(TRY003)


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

(TRY003)

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

31-34: LGTM!

The conditional installation of _audio.py for SDL3 is correctly implemented and follows the existing pattern used elsewhere in the build system.

src_c/meson.build (2)

423-433: LGTM!

The new _base_audio extension module is correctly defined following the established pattern for SDL3-specific modules.


435-457: LGTM!

The nested guard correctly disables the legacy mixer targets when building for SDL3, aligning with the new audio subsystem approach.

buildconfig/stubs/pygame/_audio.pyi (1)

17-17: Return type of load_wav needs verification against implementation.

The stub at line 17 declares the return type as tuple[AudioSpec, bytes]. Verify whether the implementation in src_py/_audio.py actually returns a tuple or a list, and align the stub accordingly if there is a mismatch.

@Starbuck5
Copy link
Member Author

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 10, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Starbuck5 Starbuck5 marked this pull request as ready for review December 10, 2025 06:58
Copy link
Contributor

@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

🧹 Nitpick comments (5)
src_c/_base_audio.c (2)

472-500: Guard against oversized buffers before casting Py_ssize_t to int

pg_audio_put_audio_stream_data now correctly uses Py_ssize_t len, but it still blindly casts to int when calling SDL_PutAudioStreamData. Very large buffers (> INT_MAX) will silently truncate and could misbehave.

Consider an explicit range check:

-    if (!SDL_PutAudioStreamData(stream, buf, (int)len)) {
+    if (len > INT_MAX) {
+        Py_DECREF(bytes);
+        return RAISE(pgExc_SDLError, "audio buffer too large");
+    }
+    if (!SDL_PutAudioStreamData(stream, buf, (int)len)) {

This keeps the SDL call well‑defined and surfaces a clear Python‑level error.


1029-1065: Tighten error handling for type creation to avoid small leaks

pg_audio_exec correctly checks both PyType_FromModuleAndSpec calls for NULL, but if PyModule_AddType fails you return -1 without DECREF’ing the just-created type object or clearing state. This only leaks a small reference during import failure, but it’s easy to clean up:

-    if (PyModule_AddType(module,
-                         (PyTypeObject *)state->audio_device_state_type) < 0) {
-        return -1;
-    }
+    if (PyModule_AddType(module,
+                         (PyTypeObject *)state->audio_device_state_type) < 0) {
+        Py_CLEAR(state->audio_device_state_type);
+        return -1;
+    }
...
-    if (PyModule_AddType(module,
-                         (PyTypeObject *)state->audio_stream_state_type) < 0) {
-        return -1;
-    }
+    if (PyModule_AddType(module,
+                         (PyTypeObject *)state->audio_stream_state_type) < 0) {
+        Py_CLEAR(state->audio_stream_state_type);
+        return -1;
+    }

Not urgent, but it aligns module init with the rest of pygame‑ce’s defensive patterns. Based on learnings, consistency with existing C modules is usually preferred.

src_py/_audio.py (2)

23-30: Annotate the registry dict as a class variable and consider safer lookup

AudioFormat._format_num_to_instance is a mutable class attribute populated from __init__ and later used by _audio_spec_from_ints. Two suggestions:

  1. Make its intent explicit and satisfy Ruff (RUF012) by annotating it as a ClassVar:
from typing import ClassVar

class AudioFormat:
    _format_num_to_instance: ClassVar[dict[int, "AudioFormat"]] = {}
  1. For _audio_spec_from_ints, consider turning the bare index into a clearer error or a fallback:
-        format_inst = AudioFormat._format_num_to_instance[format_num]
+        try:
+            format_inst = AudioFormat._format_num_to_instance[format_num]
+        except KeyError as exc:
+            raise pygame.error(f"Unknown audio format {format_num!r}") from exc

Not mandatory, but it will produce a more helpful error if SDL ever returns an unexpected format code.

Also applies to: 383-401


336-344: Clarify frequency ratio validation message

The check if value <= 0 or value > 10: correctly enforces 0 < ratio <= 10, but the message could be more direct and matches Ruff’s TRY003 suggestion better if shortened, e.g.:

-        if value <= 0 or value > 10:
-            raise ValueError(
-                "Frequency ratio must be between 0 and 10. (0 < ratio <= 10)"
-            )
+        if value <= 0 or value > 10:
+            raise ValueError("Frequency ratio must be > 0 and <= 10.")

Purely cosmetic, but improves readability.

buildconfig/stubs/mypy_allow_list.txt (1)

31-32: Escape the dot in the allowlist regex for consistency

The new allowlist entry is pygame._audio, but these patterns are regexes and existing entries escape the module dot (e.g. pygame\.ftfont). To avoid matching unintended names, consider:

-pygame._audio
+pygame\._audio

Functionally this likely makes no difference today, but it keeps the pattern style consistent.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 465a2fc and 3993882.

📒 Files selected for processing (5)
  • buildconfig/stubs/meson.build (1 hunks)
  • buildconfig/stubs/mypy_allow_list.txt (1 hunks)
  • buildconfig/stubs/pygame/_audio.pyi (1 hunks)
  • src_c/_base_audio.c (1 hunks)
  • src_py/_audio.py (1 hunks)
🧰 Additional context used
🧠 Learnings (7)
📓 Common learnings
Learnt from: Starbuck5
Repo: pygame-community/pygame-ce PR: 3573
File: src_c/_camera.c:129-131
Timestamp: 2025-08-30T21:11:00.240Z
Learning: When doing SDL2 to SDL3 compatibility changes, Starbuck5 prefers to maintain exact existing behaviors even when other improvements could be made, focusing solely on the compatibility aspect rather than mixing in behavioral fixes.
📚 Learning: 2025-10-17T08:53:32.449Z
Learnt from: damusss
Repo: pygame-community/pygame-ce PR: 3219
File: src_c/window.c:11-13
Timestamp: 2025-10-17T08:53:32.449Z
Learning: SDL3's first stable release is version 3.2.0 (released January 21, 2025). SDL 3.0.x and 3.1.x were preview/pre-release versions not intended for production use, so version checks for SDL3 features in pygame-ce should use SDL_VERSION_ATLEAST(3, 2, 0) to target the first stable release.

Applied to files:

  • buildconfig/stubs/mypy_allow_list.txt
📚 Learning: 2025-09-01T20:18:57.500Z
Learnt from: Starbuck5
Repo: pygame-community/pygame-ce PR: 3573
File: src_c/_pygame.h:108-133
Timestamp: 2025-09-01T20:18:57.500Z
Learning: In pygame-ce PR #3573, PG_SURF_BitsPerPixel was changed from a simple macro to a static inline function to handle SDL2/SDL3 differences. SDL2's BitsPerPixel includes padding bits (e.g., RGBX => 32) while SDL3's SDL_BITSPERPIXEL excludes padding (e.g., RGBX => 24). The new implementation bridges this gap rather than removing the helper entirely.

Applied to files:

  • buildconfig/stubs/mypy_allow_list.txt
  • src_c/_base_audio.c
📚 Learning: 2025-09-01T20:22:31.010Z
Learnt from: Starbuck5
Repo: pygame-community/pygame-ce PR: 3573
File: src_c/_pygame.h:115-126
Timestamp: 2025-09-01T20:22:31.010Z
Learning: In pygame-ce PR #3573, the PG_SURF_BitsPerPixel function implementation uses hardcoded return values of 32 for YUY2/UYVY/YVYU FOURCC formats because it's copying the exact logic from SDL's internal SDL_GetMasksForPixelFormat function. This ensures perfect compatibility between SDL2 and SDL3 behaviors rather than using the technically more accurate values that might be returned by SDL_GetPixelFormatDetails.

Applied to files:

  • buildconfig/stubs/mypy_allow_list.txt
📚 Learning: 2025-08-17T17:23:06.787Z
Learnt from: ankith26
Repo: pygame-community/pygame-ce PR: 3556
File: src_c/mixer.c:520-529
Timestamp: 2025-08-17T17:23:06.787Z
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.

Applied to files:

  • buildconfig/stubs/mypy_allow_list.txt
  • src_c/_base_audio.c
📚 Learning: 2025-10-08T08:15:01.411Z
Learnt from: ankith26
Repo: pygame-community/pygame-ce PR: 3602
File: buildconfig/stubs/pygame/locals.pyi:601-1198
Timestamp: 2025-10-08T08:15:01.411Z
Learning: The repository pygame-community/pygame-ce is for pygame-ce (pygame community edition), not the original pygame project. Always refer to it as pygame-ce in reviews and analyses.

Applied to files:

  • buildconfig/stubs/mypy_allow_list.txt
📚 Learning: 2025-08-13T12:39:19.130Z
Learnt from: oddbookworm
Repo: pygame-community/pygame-ce PR: 3556
File: src_c/mixer.c:806-836
Timestamp: 2025-08-13T12:39:19.130Z
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/_base_audio.c
🧬 Code graph analysis (2)
src_c/_base_audio.c (2)
src_py/_audio.py (7)
  • name (64-65)
  • name (169-170)
  • gain (189-190)
  • gain (193-194)
  • gain (325-326)
  • gain (329-333)
  • device (264-265)
src_c/base.c (2)
  • import_pygame_base (2294-2297)
  • import_pygame_rwobject (2343-2346)
src_py/_audio.py (3)
src_c/base.c (1)
  • base (2116-2277)
src_c/_base_audio.c (1)
  • _base_audio (1102-1121)
buildconfig/stubs/pygame/base.pyi (1)
  • get_sdl_byteorder (18-18)
🪛 Ruff (0.14.8)
src_py/_audio.py

23-23: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


103-105: Avoid specifying long messages outside the exception class

(TRY003)


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

(TRY003)


154-157: Avoid specifying long messages outside the exception class

(TRY003)


200-202: Avoid specifying long messages outside the exception class

(TRY003)


204-206: Avoid specifying long messages outside the exception class

(TRY003)


220-222: Avoid specifying long messages outside the exception class

(TRY003)


276-278: Avoid specifying long messages outside the exception class

(TRY003)


284-286: Avoid specifying long messages outside the exception class

(TRY003)


308-310: Avoid specifying long messages outside the exception class

(TRY003)


316-318: Avoid specifying long messages outside the exception class

(TRY003)


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

(TRY003)


342-344: Avoid specifying long messages outside the exception class

(TRY003)

🔇 Additional comments (5)
buildconfig/stubs/pygame/_audio.pyi (1)

1-164: Stub surface matches implementation; unused imports are acceptable

The _audio stubs line up with the current Python API (classes, methods, and load_wav return type). Callable/TypeVar are only used in commented-out callback signatures, which is harmless and keeps future plans documented.

No changes needed here.

buildconfig/stubs/meson.build (1)

1-6: Stub install gating for _audio.pyi looks correct

The new pg_stub_excludes list and if sdl_api != 3 guard ensure _audio.pyi is only installed for SDL3 builds while still excluding .flake8. This matches the SDL3‑only nature of the new audio API.

No changes needed.

Also applies to: 8-14

src_c/_base_audio.c (2)

215-259: AudioFormat objects passed from Python will cause PyLong_AsInt to fail

In pg_audio_open_audio_device, pg_audio_create_audio_stream, and pg_audio_set_audio_stream_format you call PyLong_AsInt on arguments that the Python layer currently passes as AudioFormat instances, not ints. PyLong_AsInt does not honor __index__, so these calls will raise TypeError instead of extracting the underlying format number, breaking AudioDevice.open() and AudioStream creation/format setters.

Consider one of:

  • In C: accept any indexable object:
PyObject *fmt_obj = args[1];
PyObject *idx = PyNumber_Index(fmt_obj);
if (idx == NULL) {
    return NULL;
}
spec.format = PyLong_AsInt(idx);
Py_DECREF(idx);
if (spec.format == -1 && PyErr_Occurred()) {
    return NULL;
}

(and similarly for the other format fields), or

  • In Python: pass int(spec.format) instead of spec.format everywhere you call into these C APIs.

Right now, without this adjustment, the core stream/device-opening API can't be used successfully.

Also applies to: 299-344, 563-603


762-788: No error handling needed for SDL_GetNumAudioDrivers()

SDL_GetNumAudioDrivers() does not return negative values on error. According to SDL documentation, it returns the count of available audio drivers or 0 if none are compiled in. Unlike SDL_GetNumAudioDevices(), this function has no error path that produces a negative return value. The suggested check is unnecessary.

Likely an incorrect or invalid review comment.

src_py/_audio.py (1)

146-157: Pass integer format codes into _base_audio, not AudioFormat instances

AudioDevice.open, AudioStream.__init__, and the src_spec/dst_spec setters pass AudioFormat objects (spec.format, value.format) into _base_audio.* calls. The C layer uses PyLong_AsInt on these arguments, which only accepts actual int objects and will raise TypeError for AudioFormat instances.

To keep the AudioFormat abstraction while making the calls work, convert to int before passing to the C backend:

-            dev_state = _base_audio.open_audio_device(
-                self._state, spec.format, spec.channels, spec.frequency
-            )
+            dev_state = _base_audio.open_audio_device(
+                self._state, int(spec.format), spec.channels, spec.frequency
+            )
...
-        self._state = _base_audio.create_audio_stream(
-            src_spec.format,
+        self._state = _base_audio.create_audio_stream(
+            int(src_spec.format),
             src_spec.channels,
             src_spec.frequency,
-            dst_spec.format,
+            int(dst_spec.format),
             dst_spec.channels,
             dst_spec.frequency,
         )
...
-        _base_audio.set_audio_stream_format(
-            self._state, (value.format, value.channels, value.frequency), None
+        _base_audio.set_audio_stream_format(
+            self._state,
+            (int(value.format), value.channels, value.frequency),
+            None,
         )
...
-        _base_audio.set_audio_stream_format(
-            self._state, None, (value.format, value.channels, value.frequency)
+        _base_audio.set_audio_stream_format(
+            self._state,
+            None,
+            (int(value.format), value.channels, value.frequency),
         )

Also applies to: 197-215, 274-291, 305-322

Copy link
Contributor

@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 (1)
src_py/_audio.py (1)

241-277: Consider avoiding duplicate get_audio_stream_format calls

Both src_spec and dst_spec call _base_audio.get_audio_stream_format(self._state) separately and then slice the tuple. You could optionally factor this into a small helper (or cache within the getter) that calls into C once and returns both specs, to avoid redundant C calls and tuple allocations, especially if these properties are accessed frequently.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3993882 and 8b076a3.

📒 Files selected for processing (3)
  • buildconfig/stubs/mypy_allow_list.txt (1 hunks)
  • src_c/_base_audio.c (1 hunks)
  • src_py/_audio.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • buildconfig/stubs/mypy_allow_list.txt
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-08-17T17:23:06.787Z
Learnt from: ankith26
Repo: pygame-community/pygame-ce PR: 3556
File: src_c/mixer.c:520-529
Timestamp: 2025-08-17T17:23:06.787Z
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.

Applied to files:

  • src_c/_base_audio.c
📚 Learning: 2025-08-13T12:39:19.130Z
Learnt from: oddbookworm
Repo: pygame-community/pygame-ce PR: 3556
File: src_c/mixer.c:806-836
Timestamp: 2025-08-13T12:39:19.130Z
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/_base_audio.c
📚 Learning: 2025-09-01T20:18:57.500Z
Learnt from: Starbuck5
Repo: pygame-community/pygame-ce PR: 3573
File: src_c/_pygame.h:108-133
Timestamp: 2025-09-01T20:18:57.500Z
Learning: In pygame-ce PR #3573, PG_SURF_BitsPerPixel was changed from a simple macro to a static inline function to handle SDL2/SDL3 differences. SDL2's BitsPerPixel includes padding bits (e.g., RGBX => 32) while SDL3's SDL_BITSPERPIXEL excludes padding (e.g., RGBX => 24). The new implementation bridges this gap rather than removing the helper entirely.

Applied to files:

  • src_c/_base_audio.c
🪛 Flake8 (7.3.0)
src_py/_audio.py

[error] 339-339: undefined name 'weakref'

(F821)

🪛 Ruff (0.14.8)
src_py/_audio.py

79-81: Avoid specifying long messages outside the exception class

(TRY003)


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

(TRY003)


128-131: Avoid specifying long messages outside the exception class

(TRY003)


174-176: Avoid specifying long messages outside the exception class

(TRY003)


178-180: Avoid specifying long messages outside the exception class

(TRY003)


194-196: Avoid specifying long messages outside the exception class

(TRY003)


250-252: Avoid specifying long messages outside the exception class

(TRY003)


258-260: Avoid specifying long messages outside the exception class

(TRY003)


282-284: Avoid specifying long messages outside the exception class

(TRY003)


290-292: Avoid specifying long messages outside the exception class

(TRY003)


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

(TRY003)


316-318: Avoid specifying long messages outside the exception class

(TRY003)


339-339: Undefined name weakref

(F821)


353-356: Avoid specifying long messages outside the exception class

(TRY003)

⏰ 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: x86_64
  • GitHub Check: i686
  • GitHub Check: AMD64
  • GitHub Check: x86
  • GitHub Check: build (macos-14)
  • GitHub Check: build (ubuntu-24.04)
  • GitHub Check: x86_64
  • GitHub Check: aarch64
  • GitHub Check: Debian (Bookworm - 12) [ppc64le]
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.10.17)
  • GitHub Check: Debian (Bookworm - 12) [s390x]
  • GitHub Check: Debian (Bookworm - 12) [armv6]
  • GitHub Check: msys2 (clang64, clang-x86_64)
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.14.0)
  • GitHub Check: build (ubuntu-22.04)
  • GitHub Check: Debian (Bookworm - 12) [armv7]
  • GitHub Check: Pyodide build
  • GitHub Check: dev-check
  • GitHub Check: msys2 (ucrt64, ucrt-x86_64)
  • GitHub Check: msys2 (mingw64, x86_64)
🔇 Additional comments (1)
src_py/_audio.py (1)

63-65: Return type and behavior of silence_value look correct

The property now returns bytes and delegates to the C helper, matching the stub and C implementation. No issues here.

Copy link
Contributor

@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 (1)
src_c/_base_audio.c (1)

575-616: Consider adding NULL checks for PyTuple_GetItem results.

While the Python wrapper likely validates that args[1] and args[2] are tuples with three elements, adding NULL checks after PyTuple_GetItem calls (lines 591-593, 601-603) would make the code more defensive and easier to debug if called incorrectly.

Example for the first tuple:

     if (!Py_IsNone(args[1])) {
-        src.format = PyLong_AsInt(PyTuple_GetItem(args[1], 0));
-        src.channels = PyLong_AsInt(PyTuple_GetItem(args[1], 1));
-        src.freq = PyLong_AsInt(PyTuple_GetItem(args[1], 2));
+        PyObject *item0 = PyTuple_GetItem(args[1], 0);
+        PyObject *item1 = PyTuple_GetItem(args[1], 1);
+        PyObject *item2 = PyTuple_GetItem(args[1], 2);
+        if (item0 == NULL || item1 == NULL || item2 == NULL) {
+            return NULL;
+        }
+        src.format = PyLong_AsInt(item0);
+        src.channels = PyLong_AsInt(item1);
+        src.freq = PyLong_AsInt(item2);
         src_p = &src;
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8b076a3 and 45e1b7d.

📒 Files selected for processing (1)
  • src_c/_base_audio.c (1 hunks)
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: Starbuck5
Repo: pygame-community/pygame-ce PR: 3573
File: src_c/_camera.c:129-131
Timestamp: 2025-08-30T21:11:00.240Z
Learning: When doing SDL2 to SDL3 compatibility changes, Starbuck5 prefers to maintain exact existing behaviors even when other improvements could be made, focusing solely on the compatibility aspect rather than mixing in behavioral fixes.
📚 Learning: 2025-08-17T17:23:06.787Z
Learnt from: ankith26
Repo: pygame-community/pygame-ce PR: 3556
File: src_c/mixer.c:520-529
Timestamp: 2025-08-17T17:23:06.787Z
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.

Applied to files:

  • src_c/_base_audio.c
📚 Learning: 2025-08-13T12:39:19.130Z
Learnt from: oddbookworm
Repo: pygame-community/pygame-ce PR: 3556
File: src_c/mixer.c:806-836
Timestamp: 2025-08-13T12:39:19.130Z
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/_base_audio.c
📚 Learning: 2025-09-01T20:18:57.500Z
Learnt from: Starbuck5
Repo: pygame-community/pygame-ce PR: 3573
File: src_c/_pygame.h:108-133
Timestamp: 2025-09-01T20:18:57.500Z
Learning: In pygame-ce PR #3573, PG_SURF_BitsPerPixel was changed from a simple macro to a static inline function to handle SDL2/SDL3 differences. SDL2's BitsPerPixel includes padding bits (e.g., RGBX => 32) while SDL3's SDL_BITSPERPIXEL excludes padding (e.g., RGBX => 24). The new implementation bridges this gap rather than removing the helper entirely.

Applied to files:

  • src_c/_base_audio.c
🧬 Code graph analysis (1)
src_c/_base_audio.c (2)
src_py/_audio.py (11)
  • name (60-61)
  • name (143-144)
  • channel_map (147-148)
  • gain (163-164)
  • gain (167-168)
  • gain (299-300)
  • gain (303-307)
  • device (238-239)
  • frequency_ratio (310-311)
  • frequency_ratio (314-320)
  • silence_value (64-65)
src_c/base.c (2)
  • import_pygame_base (2294-2297)
  • import_pygame_rwobject (2343-2346)
⏰ 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
  • GitHub Check: build (macos-14)
  • GitHub Check: build (ubuntu-24.04)
  • GitHub Check: build (ubuntu-22.04)
  • GitHub Check: Debian (Bookworm - 12) [ppc64le]
  • GitHub Check: Debian (Bookworm - 12) [armv7]
  • GitHub Check: Debian (Bookworm - 12) [s390x]
  • GitHub Check: Debian (Bookworm - 12) [armv6]
  • GitHub Check: AMD64
  • GitHub Check: dev-check
  • GitHub Check: x86
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.10.17)
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.14.0)
  • GitHub Check: msys2 (clang64, clang-x86_64)
  • GitHub Check: msys2 (ucrt64, ucrt-x86_64)
  • GitHub Check: msys2 (mingw64, x86_64)
  • GitHub Check: aarch64
  • GitHub Check: i686
  • GitHub Check: x86_64
  • GitHub Check: Pyodide build
🔇 Additional comments (9)
src_c/_base_audio.c (9)

46-59: LGTM: Conditional device closure is correct.

The check using SDL_IsAudioDevicePhysical ensures that only logical devices (returned by SDL_OpenAudioDevice) are closed, while physical device IDs from enumeration and default device constants are not. This correctly follows SDL3 API requirements.


107-144: LGTM: All error paths correctly handle cleanup.

The function now properly checks for NULL returns from PyList_New and PyLong_FromLong, and ensures SDL_free(channel_map) is called on all error paths before returning.


222-268: LGTM: SDL device cleanup on allocation failure is correct.

The function now properly closes the SDL audio device with SDL_CloseAudioDevice(logical_id) if tp_alloc fails, preventing a resource leak.


307-352: LGTM: SDL stream cleanup on allocation failure is correct.

The function now properly destroys the SDL audio stream with SDL_DestroyAudioStream(stream) if tp_alloc fails, preventing a resource leak.


479-513: LGTM: Type safety and reference counting are correct.

The function now properly uses Py_ssize_t for the length variable, checks for overflow before casting to int, and releases the bytes reference on all paths including the success path.


867-893: LGTM: Reference counting and resource cleanup are correct.

The function now uses "N" format in Py_BuildValue to transfer ownership of the bytes object, preventing a reference leak. SDL resources are properly freed.


895-929: LGTM: NULL checks after allocation are correct.

Both get_default_playback_device_state and get_default_recording_device_state now properly check if tp_alloc returns NULL before dereferencing the pointer.


1041-1078: LGTM: Type creation NULL checks are correct.

Both PyType_FromModuleAndSpec calls now have NULL checks before proceeding to add the types to the module, preventing potential crashes on allocation failure.


803-833: Missing NULL check for PyList_New.

If PyList_New(num_devices) fails at line 812, device_list will be NULL and the subsequent loop will attempt to use it in PyList_SetItem, causing undefined behavior.

Apply this diff to add the NULL check:

     PyObject *device_list = PyList_New(num_devices);
+    if (device_list == NULL) {
+        return NULL;
+    }
     PGAudioDeviceStateObject *device;
     for (int i = 0; i < num_devices; i++) {

Likely an incorrect or invalid review comment.

Copy link
Member

@ankith26 ankith26 left a comment

Choose a reason for hiding this comment

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

Thanks for taking this up and working on this 🎉 , I'm sure it was a lot of work 😅

I've given everything a quick scroll and left a few minor reviews, nothing important.

One more point of discussion I wanted to bring up since you mentioned implementing "next gen mixer" on top of this. I feel like having 3 layers of implementation is overcomplicating things to the end user. We don't want to create another draw/gfxdraw situation (this is not the best analogy but you get the idea). Ideally, the python level audio implementation should be easy enough to use that it is essentially the "next gen mixer". Maybe this means that we should consider adding more convenience methods/classes to the current audio implementation. Of course, old mixer compat could be the sole 3rd layer, but that would just be a compat layer with no new features added.

install: true,
subdir: pg,
)
endif
Copy link
Member

Choose a reason for hiding this comment

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

minor nit: this should be a part of the sdl_mixer_dep.found() if block.


static PyObject *
pg_audio_get_audio_device_name(PyObject *module, PyObject *const *args,
Py_ssize_t nargs)
Copy link
Member

Choose a reason for hiding this comment

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

if this always expects exactly one arg why not just use METH_O convention? As of now this just silently ignores extra args, and must have ub for no arg?

I also observe other places in this file where argument checking is non existent or non sufficient. I get why it is the case though, these functions are internal. But still, better error checking does no harm and catches any current/future issues in our python level more gracefully rather than segfaulting

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep that should be METH_O. (It’s one of the first functions I wrote and I didn’t adjust it to match the later style). I’m away from my computer for a few days so I will make the change when I am back or you/anyone could push that.

I intentionally omitted all argument error handling, although I used safe/stable ABI operations for all Pyobjects once received. The idea is that if checks can be moved into the Python VM that JIT Python might be smart enough to eliminate them and lead to higher performance. Not that performance is particularly important for this module.

Maybe a simple and cheap check to do would be to check the number of args is as expected?

def silence_value(self) -> bytes: ...
def __index__(self) -> int: ...
def __repr__(self) -> str: ...

Copy link
Member

Choose a reason for hiding this comment

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

I think implementing __eq__ would be handy too, so users could say spec.format == U8 or something like that.

Copy link
Member Author

Choose a reason for hiding this comment

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

This is handled by Python’s default identity comparison, because AudioFormats are singletons.

S32: AudioFormat
F32: AudioFormat

class AudioSpec:
Copy link
Member

Choose a reason for hiding this comment

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

this could be a dataclass?

Copy link
Member Author

Choose a reason for hiding this comment

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

I believe in the first draft it was, but when I actually went to write it I wanted understandable full control over all of the runtime consequences. Like I made the fields immutable. So for the typing and implementation I don’t see an advantage to making it into a dataclass.

@Starbuck5
Copy link
Member Author

One more point of discussion I wanted to bring up since you mentioned implementing "next gen mixer" on top of this. I feel like having 3 layers of implementation is overcomplicating things to the end user. We don't want to create another draw/gfxdraw situation (this is not the best analogy but you get the idea). Ideally, the python level audio implementation should be easy enough to use that it is essentially the "next gen mixer". Maybe this means that we should consider adding more convenience methods/classes to the current audio implementation. Of course, old mixer compat could be the sole 3rd layer, but that would just be a compat layer with no new features added

Good points to discuss. I believe I did say that (sounds like me), and I’m sorry to mislead, but my plan for the next generation mixer is to build it on top of SDL3_mixer, not on top of this. The audio module here is supposed to be a faithful port of SDL3’s audio module (built in). I worked up this PR so that a future mixer PR would be able to implement stuff like accepting an AudioDevice or AudioSpec parameter. This is why I didn’t put the meson build code for this under mixer-dep-found, because for this it doesn’t matter if it was found.

In terms of module proliferation pygame-ce already has an audio module, _sdl2.audio, that gives some of this enhanced functionality, like device enumeration, voice recording or streaming audio. And even though it’s completely undocumented some people still found it and used it, as we saw with a recent new contributor PR to fix something with it.

Advantages of having an audio module rather than limping into mixer:

  • Builds could exist without SDL_mixer and a lot of functionality (this module’s) would still work
  • It’s closer to SDL (when in doubt, follow SDL API)
  • It delineates nicely imo? Streams and devices vs tracks and effects.

With regards to next generation vs existing mixer APIs I was thinking they would be in the same namespace to the user, probably just implemented in a separate file and imported.

@ankith26
Copy link
Member

ankith26 commented Dec 16, 2025

oh yeah fair enough, I always confuse audio and mixer mb. Anyways, to elaborate and correct myself, what I propose is:

  • _base_audio.c for wrapping SDL3 audio as closely as possible (as you have already implemented)
  • _base_mixer.c for wrapping SDL3_mixer as closely as possible
  • pygame.audio to be a unified interface for accessing everything (tracks/effects/devices/streams/whatever) audio related (extending the current PR in the future by encorporating mixer features in the same audio python code).

Yes, I see that this implies that a SDL3_mixer-less build would have an incomplete pygame.audio implementation but I think this is an edge case we need not worry about too much given we can assume SDL3_mixer is almost always available.

@Starbuck5
Copy link
Member Author

The future module organization doesn’t need to be settled in this PR, this code would work for either strategy.

I would like to bring some of this discussion over to #3581 though.

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

Labels

mixer pygame.mixer New API This pull request may need extra debate as it adds a new class or function to pygame sdl3

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants