From da672f7d5af1964a2f64e926379736dd6e45ec17 Mon Sep 17 00:00:00 2001 From: Starbuck5 <46412508+Starbuck5@users.noreply.github.com> Date: Sun, 1 Feb 2026 01:47:29 -0800 Subject: [PATCH 1/4] Create new _sdl3_mixer module Supports SDL3 mixer constructs in close to their native form. Right now it's as a private module that only builds in SDL3. This is a stepping stone towards overall mixer support for SDL3 builds, with a compatibility layer exposing old API on top of the new, along with giving folks the new powerful stuff directly. --- .github/workflows/build-sdl3.yml | 11 + buildconfig/stubs/meson.build | 2 +- buildconfig/stubs/mypy_allow_list.txt | 1 + buildconfig/stubs/pygame/_sdl3_mixer.pyi | 184 +++ dev.py | 1 - meson.build | 2 +- src_c/_base_audio.c | 11 +- src_c/_base_audio.h | 16 + src_c/_sdl3_mixer_c.c | 1555 ++++++++++++++++++++++ src_c/meson.build | 10 +- src_py/_sdl3_mixer.py | 118 ++ src_py/meson.build | 1 + 12 files changed, 1899 insertions(+), 13 deletions(-) create mode 100644 buildconfig/stubs/pygame/_sdl3_mixer.pyi create mode 100644 src_c/_base_audio.h create mode 100644 src_c/_sdl3_mixer_c.c create mode 100644 src_py/_sdl3_mixer.py diff --git a/.github/workflows/build-sdl3.yml b/.github/workflows/build-sdl3.yml index 422dc5b381..dbb3da8e54 100644 --- a/.github/workflows/build-sdl3.yml +++ b/.github/workflows/build-sdl3.yml @@ -122,6 +122,17 @@ jobs: cmake --build . --config Release --parallel sudo cmake --install . --config Release + - name: Install SDL3_mixer + if: matrix.os != 'windows-latest' + run: | + git clone https://github.com/libsdl-org/SDL_mixer + cd SDL_mixer + mkdir build + cd build + cmake -DCMAKE_BUILD_TYPE=Release .. + cmake --build . --config Release --parallel + sudo cmake --install . --config Release + - name: Build with SDL3 run: python3 dev.py build --sdl3 diff --git a/buildconfig/stubs/meson.build b/buildconfig/stubs/meson.build index ba219e115c..522a235b0c 100644 --- a/buildconfig/stubs/meson.build +++ b/buildconfig/stubs/meson.build @@ -2,7 +2,7 @@ pg_stub_excludes = ['.flake8'] # SDL3 only! if sdl_api != 3 - pg_stub_excludes += ['_audio.pyi'] + pg_stub_excludes += ['_audio.pyi', '_sdl3_mixer.pyi'] endif install_subdir( diff --git a/buildconfig/stubs/mypy_allow_list.txt b/buildconfig/stubs/mypy_allow_list.txt index 63c2b96127..ebc18327c7 100644 --- a/buildconfig/stubs/mypy_allow_list.txt +++ b/buildconfig/stubs/mypy_allow_list.txt @@ -30,3 +30,4 @@ pygame\.docs.* # Remove me when we're checking stubs for SDL3! pygame\._audio +pygame\._sdl3_mixer diff --git a/buildconfig/stubs/pygame/_sdl3_mixer.pyi b/buildconfig/stubs/pygame/_sdl3_mixer.pyi new file mode 100644 index 0000000000..0b1ee448f0 --- /dev/null +++ b/buildconfig/stubs/pygame/_sdl3_mixer.pyi @@ -0,0 +1,184 @@ +import dataclasses +from collections.abc import Callable +from typing import Any, Type, TypedDict, TypeVar + +from pygame import _audio as audio +from pygame.typing import FileLike +from typing_extensions import Buffer + +def init() -> None: ... + +# def quit() -> None: ... +def get_sdl_mixer_version(linked: bool = True) -> tuple[int, int, int]: ... +def ms_to_frames(sample_rate: int, ms: int) -> int: ... +def frames_to_ms(sample_rate: int, frames: int) -> int: ... +def get_decoders() -> list[str]: ... + +# T = TypeVar("T") +# track_stopped_callback = Callable[[T, Track], None] +# track_mix_callback = Callable[[T, Track, audio.AudioSpec, Buffer], None] +# group_mix_callback = Callable[[T, Group, audio.AudioSpec, Buffer], None] +# post_mix_callback = Callable[[T, Mixer, audio.AudioSpec, Buffer], None] + +class Mixer: + def __init__( + self, + device: audio.AudioDevice = audio.DEFAULT_PLAYBACK_DEVICE, + spec: audio.AudioSpec | None = None, + ) -> None: ... + @property + def gain(self) -> float: ... + @gain.setter + def gain(self, value: float) -> None: ... + # TODO: implement frame kwargs, implement MIX_PROP_PLAY_FADE_IN_START_GAIN_FLOAT + def play_tag( + self, + tag: str, + loops: int = 0, + max_ms: int = -1, + start_ms: int = 0, + loop_start_ms: int = 0, + fadein_ms: int = 0, + append_silence_ms: int = 0, + ) -> None: ... + def stop_tag(self, tag: str, fade_out_ms: int = 0) -> None: ... + def pause_tag(self, tag: str) -> None: ... + def resume_tag(self, tag: str) -> None: ... + def set_tag_gain(self, tag: str, gain: float) -> None: ... + # def get_tag_tracks(self, tag: str) -> list[Track]: ... + def play_audio(self, audio: Audio) -> None: ... + def stop_all_tracks(self, fade_out_ms: int = 0) -> None: ... + def pause_all_tracks(self) -> None: ... + def resume_all_tracks(self) -> None: ... + @property + def spec(self) -> audio.AudioSpec: ... + # @property + # def frequency_ratio(self) -> float: ... + # @frequency_ratio.setter + # def frequency_ratio(self, value: float) -> None: ... + # def set_post_mix_callback( + # self, callback: post_mix_callback | None, userdata: T + # ) -> None: ... + +# class MemoryMixer(Mixer): +# def __init__(self, spec: audio.AudioSpec) -> None: ... +# def generate(self, buffer: Buffer, buflen: int) -> None: ... + +class Audio: + def __init__( + self, + file: FileLike, + predecode: bool = False, + preferred_mixer: Mixer | None = None, + ) -> None: ... + @classmethod + def from_raw( + cls, buffer: Buffer, spec: audio.AudioSpec, preferred_mixer: Mixer | None = None + ) -> Audio: ... + @classmethod + def from_sine_wave( + cls, + hz: int, + amplitude: float, + preferred_mixer: Mixer | None = None, + ms: int = -1, + ) -> Audio: ... + @property + def duration_frames(self) -> int | None: ... + @property + def duration_ms(self) -> int | None: ... + # TODO: just infinite? audio.infinite flows better I think. + @property + def duration_infinite(self) -> bool: ... + @property + def spec(self) -> audio.AudioSpec: ... + def ms_to_frames(self, ms: int) -> int: ... + def frames_to_ms(self, frames: int) -> int: ... + def get_metadata(self) -> AudioMetadata: ... + +# class Group: +# def __init__(self, mixer: Mixer) -> None: ... +# @property +# def mixer(self) -> Mixer: ... +# def set_post_mix_callback( +# self, callback: group_mix_callback | None, userdata: Type[T] +# ) -> None: ... + +class Track: + def __init__(self, mixer: Mixer) -> None: ... + # Potential idea? + # set_source(self, source: Audio | audio.AudioStream | FileLike | None) -> None: ... + # get_source for filestream could be difficult to implement in a reasonable way. + def set_audio(self, audio: Audio | None) -> None: ... + def get_audio(self) -> Audio | None: ... + def set_audiostream(self, audiostream: audio.AudioStream | None) -> None: ... + def get_audiostream(self) -> audio.AudioStream | None: ... + def set_filestream(self, file: FileLike) -> None: ... + # TODO: implement MIX_PROP_PLAY_FADE_IN_START_GAIN_FLOAT + def play( + self, + loops: int = 0, + max_frame: int = -1, + max_ms: int = -1, + start_frame: int = 0, + start_ms: int = 0, + loop_start_frame: int = 0, + loop_start_ms: int = 0, + fadein_frames: int = 0, + fadein_ms: int = 0, + append_silence_frames: int = 0, + append_silence_ms: int = 0, + ) -> None: ... + @property + def mixer(self) -> Mixer: ... + def add_tag(self, tag: str) -> None: ... + def remove_tag(self, tag: str) -> None: ... + # def get_tags(self) -> list[str]: ... + # def set_group(self, group: Group | None) -> None: ... + def set_playback_position(self, frames: int) -> None: ... + def get_playback_position(self) -> int: ... + def get_remaining_frames(self) -> int | None: ... + def ms_to_frames(self, ms: int) -> int: ... + def frames_to_ms(self, frames: int) -> int: ... + def stop(self, fade_out_frames: int = 0) -> None: ... + def pause(self) -> None: ... + def resume(self) -> None: ... + @property + def playing(self) -> bool: ... + @property + def paused(self) -> bool: ... + @property + def loops(self) -> int: ... + @property + def gain(self) -> float: ... + @gain.setter + def gain(self, value: float) -> None: ... + @property + def frequency_ratio(self) -> float: ... + @frequency_ratio.setter + def frequency_ratio(self, value: float) -> None: ... + # def set_output_channel_map(self, channel_map: list[int] | None) -> None: ... + def set_stereo(self, gains: tuple[float, float] | None) -> None: ... + def set_3d_position(self, position: tuple[float, float, float] | None) -> None: ... + def get_3d_position(self) -> tuple[float, float, float]: ... + # def set_stopped_callback( + # self, callback: track_stopped_callback | None, userdata: T + # ) -> None: ... + # def set_raw_callback( + # self, callback: track_mix_callback | None, userdata: T + # ) -> None: ... + +# class AudioDecoder: +# def __init__(self, file: FileLike) -> None: ... +# @property +# def spec(self) -> audio.AudioSpec: ... +# def decode(buffer: Buffer, spec: audio.AudioSpec) -> int: ... + +@dataclasses.dataclass(frozen=True) +class AudioMetadata: + title: str | None + artist: str | None + album: str | None + copyright: str | None + track_num: int | None + total_tracks: int | None diff --git a/dev.py b/dev.py index 63bd10e872..d62d8b1f1a 100644 --- a/dev.py +++ b/dev.py @@ -29,7 +29,6 @@ SDL3_ARGS = [ "-Csetup-args=-Dsdl_api=3", - "-Csetup-args=-Dmixer=disabled", ] COVERAGE_ARGS = ["-Csetup-args=-Dcoverage=true"] diff --git a/meson.build b/meson.build index 64bf317b7c..c437af8220 100644 --- a/meson.build +++ b/meson.build @@ -215,7 +215,7 @@ if plat == 'win' sdl_ver = (sdl_api == 3) ? '3.4.0' : '2.32.10' sdl_image_ver = (sdl_api == 3) ? '3.4.0' : '2.8.10' - sdl_mixer_ver = '2.8.2' + sdl_mixer_ver = (sdl_api == 3) ? '3.2.2' : '2.8.2' sdl_ttf_ver = (sdl_api == 3) ? '3.2.2' : '2.24.0' arch_suffix = 'x' + host_machine.cpu_family().substring(-2) diff --git a/src_c/_base_audio.c b/src_c/_base_audio.c index 48044b9fd3..cc8de4ad53 100644 --- a/src_c/_base_audio.c +++ b/src_c/_base_audio.c @@ -1,12 +1,13 @@ #include "pygame.h" #include "pgcompat.h" #include "structmember.h" +#include "_base_audio.h" // Useful heap type example @ // https://github.com/python/cpython/blob/main/Modules/xxlimited.c // *************************************************************************** -// OVERALL DEFINITIONS +// OVERALL DEFINITIONS (Also see header file) // *************************************************************************** typedef struct { @@ -17,14 +18,6 @@ typedef struct { #define GET_STATE(x) (audio_state *)PyModule_GetState(x) -typedef struct { - PyObject_HEAD SDL_AudioDeviceID devid; -} PGAudioDeviceStateObject; - -typedef struct { - PyObject_HEAD SDL_AudioStream *stream; -} PGAudioStreamStateObject; - #define AUDIO_INIT_CHECK(module) \ if (!(GET_STATE(module))->audio_initialized) { \ return RAISE(pgExc_SDLError, "audio not initialized"); \ diff --git a/src_c/_base_audio.h b/src_c/_base_audio.h new file mode 100644 index 0000000000..587d881aa3 --- /dev/null +++ b/src_c/_base_audio.h @@ -0,0 +1,16 @@ +#ifndef _PG_BASE_AUDIO_H +#define _PG_BASE_AUDIO_H + +#include "pygame.h" + +/* SHARED DEFINITIONS FOR OTHER MODULES TO BE ABLE TO SEE. */ + +typedef struct { + PyObject_HEAD SDL_AudioDeviceID devid; +} PGAudioDeviceStateObject; + +typedef struct { + PyObject_HEAD SDL_AudioStream *stream; +} PGAudioStreamStateObject; + +#endif diff --git a/src_c/_sdl3_mixer_c.c b/src_c/_sdl3_mixer_c.c new file mode 100644 index 0000000000..afa771d02a --- /dev/null +++ b/src_c/_sdl3_mixer_c.c @@ -0,0 +1,1555 @@ +#include +#include "pygame.h" +#include "pgcompat.h" +#include "_base_audio.h" + +// Useful heap type example @ +// https://github.com/python/cpython/blob/main/Modules/xxlimited.c + +// *************************************************************************** +// OVERALL DEFINITIONS +// *************************************************************************** + +typedef struct { + bool mixer_initialized; + PyObject *mixer_obj_type; + PyObject *audio_obj_type; + PyObject *track_obj_type; +} _mixer_state; + +#define GET_STATE(x) (_mixer_state *)PyModule_GetState(x) + +typedef struct { + PyObject_HEAD MIX_Mixer *mixer; +} PGMixerObject; + +typedef struct { + PyObject_HEAD MIX_Audio *audio; +} PGAudioObject; + +typedef struct { + PyObject_HEAD MIX_Track *track; + PyObject *mixer_obj; + PyObject *source_obj; +} PGTrackObject; + +// *************************************************************************** +// GLOBAL HELPER FUNCTIONS +// *************************************************************************** + +#define SET_NUM_PROPERTY_IFNOTDEFAULT_ANDFLAG(props, property, value, \ + default, success) \ + if (value != default) { \ + success &= SDL_SetNumberProperty(props, property, value); \ + } + +static bool +pg_populate_play_props(SDL_PropertiesID options, int64_t loops, + int64_t max_frame, int64_t max_ms, int64_t start_frame, + int64_t start_ms, int64_t loop_start_frame, + int64_t loop_start_ms, int64_t fadein_frames, + int64_t fadein_ms, int64_t append_silence_frames, + int64_t append_silence_ms) +{ + bool success = true; + + SET_NUM_PROPERTY_IFNOTDEFAULT_ANDFLAG(options, MIX_PROP_PLAY_LOOPS_NUMBER, + loops, 0, success); + SET_NUM_PROPERTY_IFNOTDEFAULT_ANDFLAG( + options, MIX_PROP_PLAY_MAX_FRAME_NUMBER, max_frame, -1, success); + SET_NUM_PROPERTY_IFNOTDEFAULT_ANDFLAG( + options, MIX_PROP_PLAY_MAX_MILLISECONDS_NUMBER, max_ms, -1, success); + SET_NUM_PROPERTY_IFNOTDEFAULT_ANDFLAG( + options, MIX_PROP_PLAY_START_FRAME_NUMBER, start_frame, 0, success); + SET_NUM_PROPERTY_IFNOTDEFAULT_ANDFLAG( + options, MIX_PROP_PLAY_START_MILLISECOND_NUMBER, start_ms, 0, success); + SET_NUM_PROPERTY_IFNOTDEFAULT_ANDFLAG( + options, MIX_PROP_PLAY_LOOP_START_FRAME_NUMBER, loop_start_frame, 0, + success); + SET_NUM_PROPERTY_IFNOTDEFAULT_ANDFLAG( + options, MIX_PROP_PLAY_LOOP_START_MILLISECOND_NUMBER, loop_start_ms, 0, + success); + + SET_NUM_PROPERTY_IFNOTDEFAULT_ANDFLAG(options, + MIX_PROP_PLAY_FADE_IN_FRAMES_NUMBER, + fadein_frames, 0, success); + SET_NUM_PROPERTY_IFNOTDEFAULT_ANDFLAG( + options, MIX_PROP_PLAY_FADE_IN_MILLISECONDS_NUMBER, fadein_ms, 0, + success); + SET_NUM_PROPERTY_IFNOTDEFAULT_ANDFLAG( + options, MIX_PROP_PLAY_APPEND_SILENCE_FRAMES_NUMBER, + append_silence_frames, 0, success); + SET_NUM_PROPERTY_IFNOTDEFAULT_ANDFLAG( + options, MIX_PROP_PLAY_APPEND_SILENCE_MILLISECONDS_NUMBER, + append_silence_ms, 0, success); + + return success; +} + +// *************************************************************************** +// MIXER.MIXER CLASS +// *************************************************************************** + +static PyObject * +pg_mixer_obj_play_audio(PGMixerObject *self, PyObject *args, PyObject *kwargs) +{ + PGAudioObject *audio; + char *keywords[] = {"audio", NULL}; + PyObject *audio_type = + PyObject_GetAttrString((PyObject *)self, "_audio_type"); + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O!", keywords, audio_type, + &audio)) { + Py_DECREF(audio_type); + return NULL; + } + Py_DECREF(audio_type); + + if (!MIX_PlayAudio(self->mixer, audio->audio)) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + Py_RETURN_NONE; +} + +static PyObject * +pg_mixer_obj_play_tag(PGMixerObject *self, PyObject *args, PyObject *kwargs) +{ + char *tag; + int64_t loops = 0; + int64_t max_ms = -1; + int64_t start_ms = 0, loop_start_ms = 0; + int64_t fadein_ms = 0, append_silence_ms = 0; + char *keywords[] = {"tag", + "loops", + "max_ms", + "start_ms", + "loop_start_ms", + "fadein_ms", + "append_silence_ms", + NULL}; + + if (!PyArg_ParseTupleAndKeywords( + args, kwargs, "s|LLLLLL", keywords, &tag, &loops, &max_ms, + &start_ms, &loop_start_ms, &fadein_ms, &append_silence_ms)) { + return NULL; + } + + SDL_PropertiesID options = SDL_CreateProperties(); + if (options == 0) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + // Since frames can be not meaningful between tracks with different sample + // rates, the frames arguments are not passed through here or exposed to + // Python, unlike in Track.play(). + bool success = pg_populate_play_props(options, loops, -1, max_ms, 0, + start_ms, 0, loop_start_ms, 0, + fadein_ms, 0, append_silence_ms); + + if (!success || !MIX_PlayTag(self->mixer, tag, options)) { + SDL_DestroyProperties(options); + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + SDL_DestroyProperties(options); + Py_RETURN_NONE; +} + +static PyObject * +pg_mixer_obj_stop_tag(PGMixerObject *self, PyObject *args, PyObject *kwargs) +{ + char *tag; + int64_t fade_out_ms = 0; + char *keywords[] = {"tag", "fade_out_ms", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s|L", keywords, &tag, + &fade_out_ms)) { + return NULL; + } + + if (!MIX_StopTag(self->mixer, tag, fade_out_ms)) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + Py_RETURN_NONE; +} + +static PyObject * +pg_mixer_obj_pause_tag(PGMixerObject *self, PyObject *args, PyObject *kwargs) +{ + char *tag; + char *keywords[] = {"tag", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s", keywords, &tag)) { + return NULL; + } + + if (!MIX_PauseTag(self->mixer, tag)) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + Py_RETURN_NONE; +} + +static PyObject * +pg_mixer_obj_resume_tag(PGMixerObject *self, PyObject *args, PyObject *kwargs) +{ + char *tag; + char *keywords[] = {"tag", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s", keywords, &tag)) { + return NULL; + } + + if (!MIX_ResumeTag(self->mixer, tag)) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + Py_RETURN_NONE; +} + +static PyObject * +pg_mixer_obj_set_tag_gain(PGMixerObject *self, PyObject *args, + PyObject *kwargs) +{ + char *tag; + float gain; + char *keywords[] = {"tag", "gain", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "sf", keywords, &tag, + &gain)) { + return NULL; + } + + if (!MIX_SetTagGain(self->mixer, tag, gain)) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + Py_RETURN_NONE; +} + +static PyObject * +pg_mixer_obj_stop_all_tracks(PGMixerObject *self, PyObject *args, + PyObject *kwargs) +{ + int64_t fade_out_ms = 0; + char *keywords[] = {"fade_out_ms", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|L", keywords, + &fade_out_ms)) { + return NULL; + } + + if (!MIX_StopAllTracks(self->mixer, fade_out_ms)) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + Py_RETURN_NONE; +} + +static PyObject * +pg_mixer_obj_pause_all_tracks(PGMixerObject *self, PyObject *_null) +{ + if (!MIX_PauseAllTracks(self->mixer)) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + Py_RETURN_NONE; +} + +static PyObject * +pg_mixer_obj_resume_all_tracks(PGMixerObject *self, PyObject *_null) +{ + if (!MIX_ResumeAllTracks(self->mixer)) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + Py_RETURN_NONE; +} + +static PyObject * +pg_mixer_obj_get_spec(PGMixerObject *self, PyObject *_null) +{ + SDL_AudioSpec spec; + if (!MIX_GetMixerFormat(self->mixer, &spec)) { + PyErr_SetString(pgExc_SDLError, SDL_GetError()); + return NULL; + } + + return Py_BuildValue("iii", spec.format, spec.channels, spec.freq); +} + +static int +pg_mixer_obj_init(PGMixerObject *self, PyObject *args, PyObject *kwargs) +{ + PyObject *device_obj; + PyObject *spec_obj = Py_None; + + char *keywords[] = {"device", "spec", NULL}; + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|O", keywords, + &device_obj, &spec_obj)) { + return -1; + } + + SDL_AudioSpec *spec_p = NULL; + SDL_AudioSpec spec; + // if the passed in spec obj is not None, we assume it is a correctly laid + // out tuple of elements created by the Python layer for us. + if (spec_obj != Py_None) { + spec.format = PyLong_AsInt(PyTuple_GetItem(spec_obj, 0)); + spec.channels = PyLong_AsInt(PyTuple_GetItem(spec_obj, 1)); + spec.freq = PyLong_AsInt(PyTuple_GetItem(spec_obj, 2)); + + // Check that they all succeeded + if (spec.format == -1 || spec.channels == -1 || spec.freq == -1) { + if (PyErr_Occurred()) { + return -1; + } + } + + spec_p = &spec; + } + + self->mixer = MIX_CreateMixerDevice( + ((PGAudioDeviceStateObject *)device_obj)->devid, spec_p); + if (self->mixer == NULL) { + PyErr_SetString(pgExc_SDLError, SDL_GetError()); + return -1; + } + + return 0; +} + +static void +pg_mixer_obj_dealloc(PGMixerObject *self) +{ + MIX_DestroyMixer(self->mixer); + self->mixer = NULL; + PyObject_GC_UnTrack(self); + PyTypeObject *tp = Py_TYPE(self); + freefunc free = PyType_GetSlot(tp, Py_tp_free); + free(self); + Py_DECREF(tp); +} + +static PyObject * +pg_mixer_obj_get_gain(PGMixerObject *self, void *_null) +{ + return PyFloat_FromDouble(MIX_GetMixerGain(self->mixer)); +} + +static int +pg_mixer_obj_set_gain(PGMixerObject *self, PyObject *value, void *_null) +{ + double gain = PyFloat_AsDouble(value); + if (gain == -1.0 && PyErr_Occurred()) { + return -1; + } + if (!MIX_SetMixerGain(self->mixer, (float)gain)) { + PyErr_SetString(pgExc_SDLError, SDL_GetError()); + return -1; + } + return 0; +} + +// The documentation says heap types need to support GC, so we're implementing +// traverse even though the object has no explicit references. +static int +pg_mixer_obj_traverse(PyObject *op, visitproc visit, void *arg) +{ + // Visit the type + Py_VISIT(Py_TYPE(op)); + return 0; +} + +static PyGetSetDef mixer_obj_getsets[] = { + {"gain", (getter)pg_mixer_obj_get_gain, (setter)pg_mixer_obj_set_gain, + "TODO", NULL}, + {NULL, NULL, NULL, NULL, NULL}}; + +static PyMethodDef mixer_obj_methods[] = { + {"play_tag", (PyCFunction)pg_mixer_obj_play_tag, + METH_VARARGS | METH_KEYWORDS, "TODO"}, + {"stop_tag", (PyCFunction)pg_mixer_obj_stop_tag, + METH_VARARGS | METH_KEYWORDS, "TODO"}, + {"pause_tag", (PyCFunction)pg_mixer_obj_pause_tag, + METH_VARARGS | METH_KEYWORDS, "TODO"}, + {"resume_tag", (PyCFunction)pg_mixer_obj_resume_tag, + METH_VARARGS | METH_KEYWORDS, "TODO"}, + {"set_tag_gain", (PyCFunction)pg_mixer_obj_set_tag_gain, + METH_VARARGS | METH_KEYWORDS, "TODO"}, + {"play_audio", (PyCFunction)pg_mixer_obj_play_audio, + METH_VARARGS | METH_KEYWORDS, "TODO"}, + {"stop_all_tracks", (PyCFunction)pg_mixer_obj_stop_all_tracks, + METH_VARARGS | METH_KEYWORDS, "TODO"}, + {"pause_all_tracks", (PyCFunction)pg_mixer_obj_pause_all_tracks, + METH_NOARGS, "TODO"}, + {"resume_all_tracks", (PyCFunction)pg_mixer_obj_resume_all_tracks, + METH_NOARGS, "TODO"}, + {"_get_spec", (PyCFunction)pg_mixer_obj_get_spec, METH_NOARGS, "TODO"}, + {NULL, NULL, 0, NULL}}; + +static PyType_Slot mixer_slots[] = {{Py_tp_methods, mixer_obj_methods}, + {Py_tp_init, pg_mixer_obj_init}, + {Py_tp_getset, mixer_obj_getsets}, + {Py_tp_dealloc, pg_mixer_obj_dealloc}, + {Py_tp_traverse, pg_mixer_obj_traverse}, + {0, NULL}}; + +static PyType_Spec mixer_spec = { + .name = "Mixer", + .basicsize = sizeof(PGMixerObject), + .itemsize = 0, + .flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC | Py_TPFLAGS_BASETYPE, + .slots = mixer_slots}; + +// *************************************************************************** +// MIXER.AUDIO CLASS +// *************************************************************************** + +static int +pg_audio_obj_init(PGAudioObject *self, PyObject *args, PyObject *kwargs) +{ + int predecode = 0; + PyObject *file = NULL; + PyObject *mixer_or_none = Py_None; + char *keywords[] = {"file", "predecode", "preferred_mixer", NULL}; + PyObject *mixer_type = + PyObject_GetAttrString((PyObject *)self, "_mixer_type"); + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|pO", keywords, &file, + &predecode, &mixer_or_none)) { + Py_DECREF(mixer_type); + return -1; + } + + MIX_Mixer *mixer = NULL; + if (PyObject_IsInstance(mixer_or_none, mixer_type)) { + mixer = ((PGMixerObject *)mixer_or_none)->mixer; + } + else if (!Py_IsNone(mixer_or_none)) { // not mixer, not none + Py_DECREF(mixer_type); + PyErr_SetString(PyExc_TypeError, "argument 3 must be Mixer or None"); + return -1; + } + Py_DECREF(mixer_type); + + SDL_IOStream *io = pgRWops_FromObject(file, NULL); + if (io == NULL) { + return -1; + } + + self->audio = MIX_LoadAudio_IO(mixer, io, predecode, true); + if (self->audio == NULL) { + PyErr_SetString(pgExc_SDLError, SDL_GetError()); + return -1; + } + + return 0; +} + +static void +pg_audio_obj_dealloc(PGAudioObject *self) +{ + MIX_DestroyAudio(self->audio); + self->audio = NULL; + PyObject_GC_UnTrack(self); + PyTypeObject *tp = Py_TYPE(self); + freefunc free = PyType_GetSlot(tp, Py_tp_free); + free(self); + Py_DECREF(tp); +} + +static PyObject * +pg_audio_obj_get_duration_frames(PGAudioObject *self, void *_null) +{ + int64_t duration_frames = MIX_GetAudioDuration(self->audio); + if (duration_frames < 0) { + Py_RETURN_NONE; // infinite / unknown + } + return PyLong_FromInt64(duration_frames); +} + +static PyObject * +pg_audio_obj_get_duration_ms(PGAudioObject *self, void *_null) +{ + int64_t duration_frames = MIX_GetAudioDuration(self->audio); + if (duration_frames < 0) { + Py_RETURN_NONE; // infinite / unknown + } + int64_t duration_ms = MIX_AudioFramesToMS(self->audio, duration_frames); + return PyLong_FromInt64(duration_ms); +} + +static PyObject * +pg_audio_obj_get_duration_infinite(PGAudioObject *self, void *_null) +{ + int64_t duration_frames = MIX_GetAudioDuration(self->audio); + if (duration_frames == MIX_DURATION_INFINITE) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; // not infinite / unknown +} + +static PyObject * +pg_audio_obj_from_raw(PyTypeObject *cls, PyObject *args, PyObject *kwargs) +{ + PyObject *buffer, *spec_obj; + PyObject *mixer_or_none = Py_None; + char *keywords[] = {"buffer", "spec", "preferred_mixer", NULL}; + PyObject *mixer_type = + PyObject_GetAttrString((PyObject *)cls, "_mixer_type"); + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "OO|O", keywords, &buffer, + &spec_obj, &mixer_or_none)) { + Py_DECREF(mixer_type); + return NULL; + } + + // We assume the passed spec obj is a 3-tuple of AudioSpec values + // this is validated by the Python layer. + SDL_AudioSpec spec; + spec.format = PyLong_AsInt(PyTuple_GetItem(spec_obj, 0)); + spec.channels = PyLong_AsInt(PyTuple_GetItem(spec_obj, 1)); + spec.freq = PyLong_AsInt(PyTuple_GetItem(spec_obj, 2)); + + // Check that they all succeeded + if (spec.format == -1 || spec.channels == -1 || spec.freq == -1) { + if (PyErr_Occurred()) { + Py_DECREF(mixer_type); + return NULL; + } + } + + MIX_Mixer *mixer = NULL; + if (PyObject_IsInstance(mixer_or_none, mixer_type)) { + mixer = ((PGMixerObject *)mixer_or_none)->mixer; + } + else if (!Py_IsNone(mixer_or_none)) { // not mixer, not none + Py_DECREF(mixer_type); + return RAISE(PyExc_TypeError, "argument 3 must be Mixer or None"); + } + Py_DECREF(mixer_type); + + PyObject *bytes = PyBytes_FromObject(buffer); + if (bytes == NULL) { + return NULL; + } + + void *buf; + Py_ssize_t len; + if (PyBytes_AsStringAndSize(bytes, (char **)&buf, &len) != 0) { + Py_DECREF(bytes); + return NULL; + } + + PGAudioObject *self = (PGAudioObject *)cls->tp_alloc(cls, 0); + if (self == NULL) { + Py_DECREF(bytes); + return NULL; + } + + MIX_Audio *raw_audio = MIX_LoadRawAudio(mixer, buf, (size_t)len, &spec); + if (raw_audio == NULL) { + Py_DECREF(bytes); + Py_DECREF(self); + return RAISE(pgExc_SDLError, SDL_GetError()); + } + self->audio = raw_audio; + + Py_DECREF(bytes); + return (PyObject *)self; +} + +static PyObject * +pg_audio_obj_from_sine_wave(PyTypeObject *cls, PyObject *args, + PyObject *kwargs) +{ + int hz, ms = -1; + float amplitude; + PyObject *mixer_or_none = Py_None; + char *keywords[] = {"hz", "amplitude", "preferred_mixer", "ms", NULL}; + PyObject *mixer_type = + PyObject_GetAttrString((PyObject *)cls, "_mixer_type"); + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "if|Oi", keywords, &hz, + &litude, &mixer_or_none, &ms)) { + Py_DECREF(mixer_type); + return NULL; + } + + MIX_Mixer *mixer = NULL; + if (PyObject_IsInstance(mixer_or_none, mixer_type)) { + mixer = ((PGMixerObject *)mixer_or_none)->mixer; + } + else if (!Py_IsNone(mixer_or_none)) { // not mixer, not none + Py_DECREF(mixer_type); + return RAISE(PyExc_TypeError, "argument 3 must be Mixer or None"); + } + Py_DECREF(mixer_type); + + PGAudioObject *self = (PGAudioObject *)cls->tp_alloc(cls, 0); + if (self == NULL) { + return NULL; + } + + // MIX_CreateSineWaveAudio is bugged right now (2025-10-04), + // complains about invalid context parameter. + MIX_Audio *sine_wave_audio = + MIX_CreateSineWaveAudio(mixer, hz, amplitude, ms); + if (sine_wave_audio == NULL) { + Py_DECREF(self); + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + self->audio = sine_wave_audio; + return (PyObject *)self; +} + +static PyObject * +pg_audio_obj_ms_to_frames(PGAudioObject *self, PyObject *args, + PyObject *kwargs) +{ + int64_t ms; + char *keywords[] = {"ms", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "L", keywords, &ms)) { + return NULL; + } + + int64_t frames = MIX_AudioMSToFrames(self->audio, ms); + if (frames == -1 && ms >= 0) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + return PyLong_FromInt64(frames); +} + +static PyObject * +pg_audio_obj_frames_to_ms(PGAudioObject *self, PyObject *args, + PyObject *kwargs) +{ + int64_t frames; + char *keywords[] = {"frames", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "L", keywords, &frames)) { + return NULL; + } + + int64_t ms = MIX_AudioFramesToMS(self->audio, frames); + if (ms == -1 && frames >= 0) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + return PyLong_FromInt64(ms); +} + +static PyObject * +pg_audio_obj_get_metadata(PGAudioObject *self, PyObject *_null) +{ + SDL_PropertiesID props = MIX_GetAudioProperties(self->audio); + if (props == 0) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + // Lock properties for a bit while transferring data out, for safety + if (!SDL_LockProperties(props)) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + const char *title = + SDL_GetStringProperty(props, MIX_PROP_METADATA_TITLE_STRING, NULL); + const char *artist = + SDL_GetStringProperty(props, MIX_PROP_METADATA_ARTIST_STRING, NULL); + const char *album = + SDL_GetStringProperty(props, MIX_PROP_METADATA_ALBUM_STRING, NULL); + const char *copyright = + SDL_GetStringProperty(props, MIX_PROP_METADATA_COPYRIGHT_STRING, NULL); + + PyObject *track_obj; + if (SDL_GetPropertyType(props, MIX_PROP_METADATA_TRACK_NUMBER) == + SDL_PROPERTY_TYPE_NUMBER) { + int64_t track_no = + SDL_GetNumberProperty(props, MIX_PROP_METADATA_TRACK_NUMBER, 0); + track_obj = PyLong_FromInt64(track_no); + } + else { + track_obj = Py_NewRef(Py_None); + } + + PyObject *total_track_obj; + if (SDL_GetPropertyType(props, MIX_PROP_METADATA_TOTAL_TRACKS_NUMBER) == + SDL_PROPERTY_TYPE_NUMBER) { + int64_t track_no = SDL_GetNumberProperty( + props, MIX_PROP_METADATA_TOTAL_TRACKS_NUMBER, 0); + total_track_obj = PyLong_FromInt64(track_no); + } + else { + total_track_obj = Py_NewRef(Py_None); + } + + SDL_UnlockProperties(props); + + PyObject *meta_dict = + Py_BuildValue("{sz sz sz sz sN sN}", "title", title, "artist", artist, + "album", album, "copyright", copyright, "track_num", + track_obj, "total_tracks", total_track_obj); + + return meta_dict; +} + +static PyObject * +pg_audio_obj_get_spec(PGAudioObject *self, PyObject *_null) +{ + SDL_AudioSpec spec; + if (!MIX_GetAudioFormat(self->audio, &spec)) { + PyErr_SetString(pgExc_SDLError, SDL_GetError()); + return NULL; + } + + return Py_BuildValue("iii", spec.format, spec.channels, spec.freq); +} + +static int +pg_audio_obj_traverse(PyObject *op, visitproc visit, void *arg) +{ + // Visit the type + Py_VISIT(Py_TYPE(op)); + return 0; +} + +static PyGetSetDef audio_obj_getsets[] = { + {"duration_frames", (getter)pg_audio_obj_get_duration_frames, NULL, "TODO", + NULL}, + {"duration_ms", (getter)pg_audio_obj_get_duration_ms, NULL, "TODO", NULL}, + {"duration_infinite", (getter)pg_audio_obj_get_duration_infinite, NULL, + "TODO", NULL}, + {NULL, NULL, NULL, NULL, NULL}}; + +static PyMethodDef audio_obj_methods[] = { + {"from_sine_wave", (PyCFunction)pg_audio_obj_from_sine_wave, + METH_CLASS | METH_VARARGS | METH_KEYWORDS, "TODO"}, + {"from_raw", (PyCFunction)pg_audio_obj_from_raw, + METH_CLASS | METH_VARARGS | METH_KEYWORDS, "TODO"}, + {"ms_to_frames", (PyCFunction)pg_audio_obj_ms_to_frames, + METH_VARARGS | METH_KEYWORDS, "TODO"}, + {"frames_to_ms", (PyCFunction)pg_audio_obj_frames_to_ms, + METH_VARARGS | METH_KEYWORDS, "TODO"}, + {"get_metadata", (PyCFunction)pg_audio_obj_get_metadata, METH_NOARGS, + "TODO"}, + {"_get_spec", (PyCFunction)pg_audio_obj_get_spec, METH_NOARGS, "TODO"}, + {NULL, NULL, 0, NULL}}; + +static PyType_Slot audio_slots[] = {{Py_tp_init, pg_audio_obj_init}, + {Py_tp_getset, audio_obj_getsets}, + {Py_tp_methods, audio_obj_methods}, + {Py_tp_dealloc, pg_audio_obj_dealloc}, + {Py_tp_traverse, pg_audio_obj_traverse}, + {0, NULL}}; + +static PyType_Spec audio_spec = { + .name = "Audio", + .basicsize = sizeof(PGAudioObject), + .itemsize = 0, + .flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC | Py_TPFLAGS_BASETYPE, + .slots = audio_slots}; + +// *************************************************************************** +// MIXER.TRACK CLASS +// *************************************************************************** + +static int +pg_track_obj_init(PGTrackObject *self, PyObject *args, PyObject *kwargs) +{ + PGMixerObject *mixer = NULL; + char *keywords[] = {"mixer", NULL}; + + // Input object type check handled at the Python level. + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O", keywords, &mixer)) { + return -1; + } + + self->track = MIX_CreateTrack(mixer->mixer); + if (self->track == NULL) { + PyErr_SetString(pgExc_SDLError, SDL_GetError()); + return -1; + } + + // Mixers own Tracks. When the Mixer is deallocated, the tracks become + // invalid. So we need to hold a reference to prevent Mixer deallocating + // before any of the Tracks it owns. + Py_INCREF(mixer); + self->mixer_obj = (PyObject *)mixer; + + return 0; +} + +static void +pg_track_obj_dealloc(PGTrackObject *self) +{ + MIX_DestroyTrack(self->track); + self->track = NULL; + PyObject_GC_UnTrack(self); + Py_CLEAR(self->mixer_obj); + Py_CLEAR(self->source_obj); + PyTypeObject *tp = Py_TYPE(self); + freefunc free = PyType_GetSlot(tp, Py_tp_free); + free(self); + Py_DECREF(tp); +} + +static PyObject * +pg_track_obj_get_mixer(PGTrackObject *self, PyObject *_null) +{ + Py_INCREF(self->mixer_obj); + return self->mixer_obj; +} + +static PyObject * +pg_track_obj_get_playing(PGTrackObject *self, PyObject *_null) +{ + return PyBool_FromLong(MIX_TrackPlaying(self->track)); +} + +static PyObject * +pg_track_obj_get_paused(PGTrackObject *self, PyObject *_null) +{ + return PyBool_FromLong(MIX_TrackPaused(self->track)); +} + +static PyObject * +pg_track_obj_get_loops(PGTrackObject *self, PyObject *_null) +{ + return PyLong_FromLong(MIX_GetTrackLoops(self->track)); +} + +static PyObject * +pg_track_obj_get_gain(PGTrackObject *self, PyObject *_null) +{ + return PyFloat_FromDouble(MIX_GetTrackGain(self->track)); +} + +static int +pg_track_obj_set_gain(PGTrackObject *self, PyObject *value, void *_null) +{ + double gain = PyFloat_AsDouble(value); + if (gain == -1.0 && PyErr_Occurred()) { + return -1; + } + if (!MIX_SetTrackGain(self->track, (float)gain)) { + PyErr_SetString(pgExc_SDLError, SDL_GetError()); + return -1; + } + return 0; +} + +static PyObject * +pg_track_obj_get_freq_ratio(PGTrackObject *self, PyObject *_null) +{ + float ratio = MIX_GetTrackFrequencyRatio(self->track); + if (ratio == 0.0f) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + return PyFloat_FromDouble((double)ratio); +} + +static int +pg_track_obj_set_freq_ratio(PGTrackObject *self, PyObject *value, void *_null) +{ + double ratio = PyFloat_AsDouble(value); + if (ratio == -1.0 && PyErr_Occurred()) { + return -1; + } + if (!MIX_SetTrackFrequencyRatio(self->track, (float)ratio)) { + PyErr_SetString(pgExc_SDLError, SDL_GetError()); + return -1; + } + return 0; +} + +static PyObject * +pg_track_obj_set_audio(PGTrackObject *self, PyObject *args, PyObject *kwargs) +{ + PyObject *audio_or_none = NULL; + char *keywords[] = {"audio", NULL}; + PyObject *audio_type = + PyObject_GetAttrString((PyObject *)self, "_audio_type"); + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O", keywords, + &audio_or_none)) { + Py_DECREF(audio_type); + return NULL; + } + + MIX_Audio *audio = NULL; + if (PyObject_IsInstance(audio_or_none, audio_type)) { // audio + audio = ((PGAudioObject *)audio_or_none)->audio; + } + else if (!Py_IsNone(audio_or_none)) { // not audio, not none + Py_DECREF(audio_type); + return RAISE(PyExc_TypeError, "argument 1 must be Audio or None"); + } + Py_DECREF(audio_type); + + if (!MIX_SetTrackAudio(self->track, audio)) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + // We've successfully added (or removed) an audio, lets decref anything + // we were previously holding onto. + Py_CLEAR(self->source_obj); + + if (audio != NULL) { + // We've successfully added an audio object, yay! + Py_INCREF(audio_or_none); + self->source_obj = audio_or_none; + } + + Py_RETURN_NONE; +} + +static PyObject * +pg_track_obj_get_audio(PGTrackObject *self, PyObject *_null) +{ + if (MIX_GetTrackAudio(self->track) != NULL) { + // This track object owns an audio, therefore our source object must + // be non-null, and an audio object. + Py_INCREF(self->source_obj); + return self->source_obj; + } + + Py_RETURN_NONE; +} + +static PyObject * +pg_track_obj_set_audiostream(PGTrackObject *self, PyObject *args, + PyObject *kwargs) +{ + PyObject *audiostream_or_none = NULL; + char *keywords[] = {"audiostream", NULL}; + + // This function relies on Python level type checking to remove values + // that are not AudioStream objects or None. + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O", keywords, + &audiostream_or_none)) { + return NULL; + } + + SDL_AudioStream *stream = NULL; + if (audiostream_or_none != Py_None) { + // audiostream._state to get at internals + PGAudioStreamStateObject *as_state = + (PGAudioStreamStateObject *)PyObject_GetAttrString( + audiostream_or_none, "_state"); + if (as_state == NULL) { + return RAISE(pgExc_SDLError, + "Unexpected internal error getting SDL audio stream " + "from Python object"); + } + stream = as_state->stream; + Py_DECREF(as_state); // PyObject_GetAttrString gives new ref + } + + if (!MIX_SetTrackAudioStream(self->track, stream)) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + // We've potentially replaced the track source, so lets get + // rid of any previous track source reference. + Py_CLEAR(self->source_obj); + + if (stream != NULL) { + // We've successfully added an audio object, yay! + Py_INCREF(audiostream_or_none); + self->source_obj = audiostream_or_none; + } + + Py_RETURN_NONE; +} + +static PyObject * +pg_track_obj_get_audiostream(PGTrackObject *self, PyObject *_null) +{ + if (MIX_GetTrackAudioStream(self->track) != NULL) { + // This track object owns an audio, therefore our source object must + // be non-null, and an audio object. + Py_INCREF(self->source_obj); + return self->source_obj; + } + + Py_RETURN_NONE; +} + +static PyObject * +pg_track_obj_set_filestream(PGTrackObject *self, PyObject *args, + PyObject *kwargs) +{ + PyObject *file_obj = NULL; + char *keywords[] = {"file", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O", keywords, &file_obj)) { + return NULL; + } + + SDL_IOStream *io = pgRWops_FromObject(file_obj, NULL); + if (io == NULL) { + return NULL; + } + + if (!MIX_SetTrackIOStream(self->track, io, true)) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + // We've potentially replaced the track source, so lets get + // rid of any previous track source reference. + Py_CLEAR(self->source_obj); + + // Hold onto you! -- is this actually needed? + // Theoretically this is keeping Python file object (like BytesIO) alive + // through the stream, but maybe the rwObject subsystem is smart enough + // to do that. + Py_INCREF(file_obj); + self->source_obj = file_obj; + + Py_RETURN_NONE; +} + +static PyObject * +pg_track_obj_play(PGTrackObject *self, PyObject *args, PyObject *kwargs) +{ + int64_t loops = 0; + int64_t max_frame = -1, max_ms = -1; + int64_t start_frame = 0, start_ms = 0; + int64_t loop_start_frame = 0, loop_start_ms = 0; + int64_t fadein_frames = 0, fadein_ms = 0; + int64_t append_silence_frames = 0, append_silence_ms = 0; + char *keywords[] = {"loops", + "max_frame", + "max_ms", + "start_frame", + "start_ms", + "loop_start_frame", + "loop_start_ms", + "fadein_frames", + "fadein_ms", + "append_silence_frames", + "append_silence_ms", + NULL}; + + if (!PyArg_ParseTupleAndKeywords( + args, kwargs, "|LLLLLLLLLLL", keywords, &loops, &max_frame, + &max_ms, &start_frame, &start_ms, &loop_start_frame, + &loop_start_ms, &fadein_frames, &fadein_ms, &append_silence_frames, + &append_silence_ms)) { + return NULL; + } + + SDL_PropertiesID options = SDL_CreateProperties(); + if (options == 0) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + bool success = pg_populate_play_props( + options, loops, max_frame, max_ms, start_frame, start_ms, + loop_start_frame, loop_start_ms, fadein_frames, fadein_ms, + append_silence_frames, append_silence_ms); + + if (!success || !MIX_PlayTrack(self->track, options)) { + SDL_DestroyProperties(options); + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + SDL_DestroyProperties(options); + Py_RETURN_NONE; +} + +static PyObject * +pg_track_obj_add_tag(PGTrackObject *self, PyObject *args, PyObject *kwargs) +{ + char *tag; + char *keywords[] = {"tag", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s", keywords, &tag)) { + return NULL; + } + + if (!MIX_TagTrack(self->track, tag)) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + Py_RETURN_NONE; +} + +static PyObject * +pg_track_obj_remove_tag(PGTrackObject *self, PyObject *args, PyObject *kwargs) +{ + char *tag; + char *keywords[] = {"tag", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s", keywords, &tag)) { + return NULL; + } + + MIX_UntagTrack(self->track, tag); // no error return! + Py_RETURN_NONE; +} + +static PyObject * +pg_track_obj_set_playback_position(PGTrackObject *self, PyObject *args, + PyObject *kwargs) +{ + int64_t frame_position; + char *keywords[] = {"frames", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "L", keywords, + &frame_position)) { + return NULL; + } + + if (!MIX_SetTrackPlaybackPosition(self->track, frame_position)) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + Py_RETURN_NONE; +} + +static PyObject * +pg_track_obj_get_playback_position(PGTrackObject *self, PyObject *null) +{ + int64_t frame_position = MIX_GetTrackPlaybackPosition(self->track); + if (frame_position == -1) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + return PyLong_FromInt64(frame_position); +} + +static PyObject * +pg_track_obj_get_remaining_frames(PGTrackObject *self, PyObject *null) +{ + int64_t remaining = MIX_GetTrackRemaining(self->track); + + // If unknown, return None + if (remaining == -1) { + Py_RETURN_NONE; + } + + return PyLong_FromInt64(remaining); +} + +static PyObject * +pg_track_obj_ms_to_frames(PGTrackObject *self, PyObject *args, + PyObject *kwargs) +{ + int64_t ms; + char *keywords[] = {"ms", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "L", keywords, &ms)) { + return NULL; + } + + int64_t frames = MIX_TrackMSToFrames(self->track, ms); + if (frames == -1 && ms >= 0) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + return PyLong_FromInt64(frames); +} + +static PyObject * +pg_track_obj_frames_to_ms(PGTrackObject *self, PyObject *args, + PyObject *kwargs) +{ + int64_t frames; + char *keywords[] = {"frames", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "L", keywords, &frames)) { + return NULL; + } + + int64_t ms = MIX_TrackFramesToMS(self->track, frames); + if (ms == -1 && frames >= 0) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + return PyLong_FromInt64(ms); +} + +static PyObject * +pg_track_obj_stop(PGTrackObject *self, PyObject *args, PyObject *kwargs) +{ + int64_t fade_out_frames = 0; + char *keywords[] = {"fade_out_frames", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|L", keywords, + &fade_out_frames)) { + return NULL; + } + + if (!MIX_StopTrack(self->track, fade_out_frames)) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + Py_RETURN_NONE; +} + +static PyObject * +pg_track_obj_pause(PGTrackObject *self, PyObject *null) +{ + if (!MIX_PauseTrack(self->track)) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + Py_RETURN_NONE; +} + +static PyObject * +pg_track_obj_resume(PGTrackObject *self, PyObject *null) +{ + if (!MIX_ResumeTrack(self->track)) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + Py_RETURN_NONE; +} + +static PyObject * +pg_track_obj_set_stereo(PGTrackObject *self, PyObject *args, PyObject *kwargs) +{ + PyObject *gains_or_none_obj = NULL; + char *keywords[] = {"gains", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O", keywords, + &gains_or_none_obj)) { + return NULL; + } + + MIX_StereoGains *gains_p = NULL; + MIX_StereoGains gains; + if (gains_or_none_obj != Py_None) { + if (!pg_TwoFloatsFromObj(gains_or_none_obj, &gains.left, + &gains.right)) { + return RAISE(PyExc_TypeError, + "gains must be a sequence of two numbers"); + } + gains_p = &gains; + } + + if (!MIX_SetTrackStereo(self->track, gains_p)) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + Py_RETURN_NONE; +} + +static PyObject * +pg_track_obj_set_3d_position(PGTrackObject *self, PyObject *args, + PyObject *kwargs) +{ + PyObject *position_or_none_obj = NULL; + char *keywords[] = {"position", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O", keywords, + &position_or_none_obj)) { + return NULL; + } + + MIX_Point3D *point_p = NULL; + MIX_Point3D point; + if (position_or_none_obj != Py_None) { + // The error message this raises with invalid input not entirely ideal + if (!PyArg_ParseTuple(position_or_none_obj, "fff", &point.x, &point.y, + &point.z)) { + return NULL; + } + point_p = &point; + } + + if (!MIX_SetTrack3DPosition(self->track, point_p)) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + Py_RETURN_NONE; +} + +static PyObject * +pg_track_obj_get_3d_position(PGTrackObject *self, PyObject *null) +{ + MIX_Point3D point; + + if (!MIX_GetTrack3DPosition(self->track, &point)) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + return Py_BuildValue("fff", point.x, point.y, point.z); +} + +// traverse: Visit all references from an object, including its type +static int +pg_track_obj_traverse(PyObject *op, visitproc visit, void *arg) +{ + // Visit the type + Py_VISIT(Py_TYPE(op)); + + PGTrackObject *self = (PGTrackObject *)op; + Py_VISIT(self->mixer_obj); + Py_VISIT(self->source_obj); + return 0; +} + +static int +pg_track_obj_clear(PyObject *op) +{ + PGTrackObject *self = (PGTrackObject *)op; + Py_CLEAR(self->mixer_obj); + Py_CLEAR(self->source_obj); + return 0; +} + +static PyGetSetDef track_obj_getsets[] = { + {"mixer", (getter)pg_track_obj_get_mixer, NULL, "TODO", NULL}, + {"playing", (getter)pg_track_obj_get_playing, NULL, "TODO", NULL}, + {"paused", (getter)pg_track_obj_get_paused, NULL, "TODO", NULL}, + {"loops", (getter)pg_track_obj_get_loops, NULL, "TODO", NULL}, + {"gain", (getter)pg_track_obj_get_gain, (setter)pg_track_obj_set_gain, + "TODO", NULL}, + {"frequency_ratio", (getter)pg_track_obj_get_freq_ratio, + (setter)pg_track_obj_set_freq_ratio, "TODO", NULL}, + {NULL, NULL, NULL, NULL, NULL}}; + +static PyMethodDef track_obj_methods[] = { + {"set_audio", (PyCFunction)pg_track_obj_set_audio, + METH_VARARGS | METH_KEYWORDS, "TODO"}, + {"get_audio", (PyCFunction)pg_track_obj_get_audio, METH_NOARGS, "TODO"}, + {"set_audiostream", (PyCFunction)pg_track_obj_set_audiostream, + METH_VARARGS | METH_KEYWORDS, "TODO"}, + {"get_audiostream", (PyCFunction)pg_track_obj_get_audiostream, METH_NOARGS, + "TODO"}, + {"set_filestream", (PyCFunction)pg_track_obj_set_filestream, + METH_VARARGS | METH_KEYWORDS, "TODO"}, + {"play", (PyCFunction)pg_track_obj_play, METH_VARARGS | METH_KEYWORDS, + "TODO"}, + {"add_tag", (PyCFunction)pg_track_obj_add_tag, + METH_VARARGS | METH_KEYWORDS, "TODO"}, + {"remove_tag", (PyCFunction)pg_track_obj_remove_tag, + METH_VARARGS | METH_KEYWORDS, "TODO"}, + {"set_playback_position", (PyCFunction)pg_track_obj_set_playback_position, + METH_VARARGS | METH_KEYWORDS, "TODO"}, + {"get_playback_position", (PyCFunction)pg_track_obj_get_playback_position, + METH_NOARGS, "TODO"}, + {"get_remaining_frames", (PyCFunction)pg_track_obj_get_remaining_frames, + METH_NOARGS, "TODO"}, + {"ms_to_frames", (PyCFunction)pg_track_obj_ms_to_frames, + METH_VARARGS | METH_KEYWORDS, "TODO"}, + {"frames_to_ms", (PyCFunction)pg_track_obj_frames_to_ms, + METH_VARARGS | METH_KEYWORDS, "TODO"}, + {"stop", (PyCFunction)pg_track_obj_stop, METH_VARARGS | METH_KEYWORDS, + "TODO"}, + {"pause", (PyCFunction)pg_track_obj_pause, METH_NOARGS, "TODO"}, + {"resume", (PyCFunction)pg_track_obj_resume, METH_NOARGS, "TODO"}, + {"set_stereo", (PyCFunction)pg_track_obj_set_stereo, + METH_VARARGS | METH_KEYWORDS, "TODO"}, + {"set_3d_position", (PyCFunction)pg_track_obj_set_3d_position, + METH_VARARGS | METH_KEYWORDS, "TODO"}, + {"get_3d_position", (PyCFunction)pg_track_obj_get_3d_position, METH_NOARGS, + "TODO"}, + {NULL, NULL, 0, NULL}}; + +static PyType_Slot track_slots[] = {{Py_tp_init, pg_track_obj_init}, + {Py_tp_dealloc, pg_track_obj_dealloc}, + {Py_tp_getset, track_obj_getsets}, + {Py_tp_methods, track_obj_methods}, + {Py_tp_traverse, pg_track_obj_traverse}, + {Py_tp_clear, pg_track_obj_clear}, + {0, NULL}}; + +static PyType_Spec track_spec = { + .name = "Track", + .basicsize = sizeof(PGTrackObject), + .itemsize = 0, + .flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC | Py_TPFLAGS_BASETYPE, + .slots = track_slots}; + +// *************************************************************************** +// MODULE METHODS +// *************************************************************************** + +static PyObject * +pg_mixer_init(PyObject *module, PyObject *_null) +{ + _mixer_state *state = GET_STATE(module); + if (!state->mixer_initialized) { + if (!MIX_Init()) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + state->mixer_initialized = true; + } + Py_RETURN_NONE; +} + +static PyObject * +pg_mixer_quit(PyObject *module, PyObject *_null) +{ + _mixer_state *state = GET_STATE(module); + if (state->mixer_initialized) { + MIX_Quit(); + state->mixer_initialized = false; + } + Py_RETURN_NONE; +} + +static PyObject * +pg_mixer_get_sdl_mixer_version(PyObject *self, PyObject *args, + PyObject *kwargs) +{ + int linked = 1; /* Default is linked version. */ + int version = SDL_MIXER_VERSION; + + char *keywords[] = {"linked", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|p", keywords, &linked)) { + return NULL; /* Exception already set. */ + } + + if (linked) { + version = MIX_Version(); + } + + return Py_BuildValue("iii", PG_FIND_VNUM_MAJOR(version), + PG_FIND_VNUM_MINOR(version), + PG_FIND_VNUM_MICRO(version)); +} + +static PyObject * +pg_mixer_get_decoders(PyObject *module, PyObject *_null) +{ + _mixer_state *state = GET_STATE(module); + if (!state->mixer_initialized) { + return RAISE(pgExc_SDLError, "mixer not initialized"); + } + + int num_decoders = MIX_GetNumAudioDecoders(); + PyObject *decoders = PyList_New(num_decoders); + if (decoders == NULL) { + return NULL; // error already set + } + + for (int i = 0; i < num_decoders; i++) { + PyObject *decoder = PyUnicode_FromString(MIX_GetAudioDecoder(i)); + if (decoder == NULL || PyList_SetItem(decoders, i, decoder)) { + Py_DECREF(decoders); + return NULL; // error already set + } + } + + return decoders; +} + +static PyMethodDef _mixer_methods[] = { + {"init", (PyCFunction)pg_mixer_init, METH_NOARGS, "DOC_MIXER_INIT"}, + {"quit", (PyCFunction)pg_mixer_quit, METH_NOARGS, "DOC_MIXER_QUIT"}, + {"get_sdl_mixer_version", (PyCFunction)pg_mixer_get_sdl_mixer_version, + METH_VARARGS | METH_KEYWORDS, "TODO"}, + {"get_decoders", (PyCFunction)pg_mixer_get_decoders, METH_NOARGS, "TODO"}, + {NULL, NULL, 0, NULL}}; + +// *************************************************************************** +// MODULE SETUP +// *************************************************************************** + +int +pg_audio_exec(PyObject *module) +{ + /*imported needed apis*/ + import_pygame_base(); + if (PyErr_Occurred()) { + return -1; + } + import_pygame_rwobject(); + if (PyErr_Occurred()) { + return -1; + } + + _mixer_state *state = GET_STATE(module); + state->mixer_initialized = false; + + // Create types + state->mixer_obj_type = + PyType_FromModuleAndSpec(module, &mixer_spec, NULL); + state->audio_obj_type = + PyType_FromModuleAndSpec(module, &audio_spec, NULL); + state->track_obj_type = + PyType_FromModuleAndSpec(module, &track_spec, NULL); + + // If any NULLs, error + if (state->mixer_obj_type == NULL || state->audio_obj_type == NULL || + state->track_obj_type == NULL) { + return -1; + } + + // Add types to module + if (PyModule_AddType(module, (PyTypeObject *)state->mixer_obj_type) < 0 || + PyModule_AddType(module, (PyTypeObject *)state->audio_obj_type) < 0 || + PyModule_AddType(module, (PyTypeObject *)state->track_obj_type) < 0) { + return -1; + } + + // Add references between types where necessary. + if (PyObject_SetAttrString(state->mixer_obj_type, "_audio_type", + state->audio_obj_type) < 0 || + PyObject_SetAttrString(state->track_obj_type, "_audio_type", + state->audio_obj_type) < 0 || + PyObject_SetAttrString(state->audio_obj_type, "_mixer_type", + state->mixer_obj_type) < 0) { + return -1; + } + + return 0; +} + +static int +pg_mixer_traverse(PyObject *module, visitproc visit, void *arg) +{ + _mixer_state *state = GET_STATE(module); + Py_VISIT(state->mixer_obj_type); + Py_VISIT(state->audio_obj_type); + Py_VISIT(state->track_obj_type); + return 0; +} + +static int +pg_mixer_clear(PyObject *module) +{ + _mixer_state *state = GET_STATE(module); + Py_CLEAR(state->mixer_obj_type); + Py_CLEAR(state->audio_obj_type); + Py_CLEAR(state->track_obj_type); + return 0; +} + +static void +pg_mixer_free(void *module) +{ + // allow pg_audio_exec to omit calling pg_audio_clear on error + (void)pg_mixer_clear((PyObject *)module); +} + +MODINIT_DEFINE(_sdl3_mixer_c) +{ + static PyModuleDef_Slot mixer_slots[] = { + {Py_mod_exec, &pg_audio_exec}, +#if PY_VERSION_HEX >= 0x030c0000 + {Py_mod_multiple_interpreters, + Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED}, // TODO: see if this can + // be supported later +#endif +#if PY_VERSION_HEX >= 0x030d0000 + {Py_mod_gil, Py_MOD_GIL_USED}, // TODO: support this later +#endif + {0, NULL}}; + static struct PyModuleDef _module = {PyModuleDef_HEAD_INIT, + .m_name = "_sdl3_mixer_c", + .m_doc = NULL, + .m_size = sizeof(_mixer_state), + .m_methods = _mixer_methods, + .m_slots = mixer_slots, + .m_traverse = pg_mixer_traverse, + .m_clear = pg_mixer_clear, + .m_free = pg_mixer_free}; + + return PyModuleDef_Init(&_module); +} diff --git a/src_c/meson.build b/src_c/meson.build index 13aa48c7b6..e142630abf 100644 --- a/src_c/meson.build +++ b/src_c/meson.build @@ -452,8 +452,16 @@ if sdl_api != 3 install: true, subdir: pg, ) +else + _mixer = py.extension_module( + '_sdl3_mixer_c', + '_sdl3_mixer_c.c', + c_args: warnings_error, + dependencies: pg_base_deps + sdl_mixer_dep, + install: true, + subdir: pg, + ) endif - endif if freetype_dep.found() diff --git a/src_py/_sdl3_mixer.py b/src_py/_sdl3_mixer.py new file mode 100644 index 0000000000..638e782340 --- /dev/null +++ b/src_py/_sdl3_mixer.py @@ -0,0 +1,118 @@ +import dataclasses + +from pygame import _audio as audio, _sdl3_mixer_c # pylint: disable=no-name-in-module + +init = _sdl3_mixer_c.init +# quit = _sdl3_mixer_c.quit +get_sdl_mixer_version = _sdl3_mixer_c.get_sdl_mixer_version + + +# Pure Python version of MIX_MSToFrames, since it's so straightforward. +def ms_to_frames(sample_rate: int, ms: int) -> int: + if sample_rate <= 0: + raise ValueError("Sample rate must be greater than zero.") + if ms < 0: + raise ValueError("MS must be non-negative.") + + return int(ms / 1000 * sample_rate) + + +# Pure Python version of MIX_FramesToMS, since it's so straightforward. +def frames_to_ms(sample_rate: int, frames: int) -> int: + if sample_rate <= 0: + raise ValueError("Sample rate must be greater than zero.") + if frames < 0: + raise ValueError("Frames must be non-negative.") + + return int(frames / sample_rate * 1000) + + +get_decoders = _sdl3_mixer_c.get_decoders + + +class Mixer(_sdl3_mixer_c.Mixer): + def __init__( + self, + device: audio.AudioDevice = audio.DEFAULT_PLAYBACK_DEVICE, + spec: audio.AudioSpec | None = None, + ) -> None: + if spec is None: + _sdl3_mixer_c.Mixer.__init__(self, device._state, spec) + elif isinstance(spec, audio.AudioSpec): + _sdl3_mixer_c.Mixer.__init__( + self, device._state, (spec.format, spec.channels, spec.frequency) + ) + else: + raise TypeError( + "Mixer init 'spec' argument must be an AudioSpec " + f"or None, received {type(spec)}" + ) + + @property + def spec(self) -> audio.AudioSpec: + return audio._internals.audio_spec_from_ints( + *_sdl3_mixer_c.Mixer._get_spec(self) + ) + + +@dataclasses.dataclass(frozen=True) +class AudioMetadata: + title: str | None + artist: str | None + album: str | None + copyright: str | None + track_num: int | None + total_tracks: int | None + + +class Audio(_sdl3_mixer_c.Audio): + @classmethod + def from_raw( + cls, buffer, spec: audio.AudioSpec, preferred_mixer: Mixer | None = None + ): + if not isinstance(spec, audio.AudioSpec): + raise TypeError( + f"Audio 'spec' argument must be an AudioSpec, received {type(spec)}" + ) + + return super().from_raw( + buffer, (spec.format, spec.channels, spec.frequency), preferred_mixer + ) + + @property + def spec(self) -> audio.AudioSpec: + return audio._internals.audio_spec_from_ints( + *_sdl3_mixer_c.Audio._get_spec(self) + ) + + def get_metadata(self) -> AudioMetadata: + metadata = _sdl3_mixer_c.Audio.get_metadata(self) + return AudioMetadata( + title=metadata["title"], + artist=metadata["artist"], + album=metadata["album"], + copyright=metadata["copyright"], + track_num=metadata["track_num"], + total_tracks=metadata["total_tracks"], + ) + + +class Track(_sdl3_mixer_c.Track): + def __init__(self, mixer: Mixer) -> None: + if not isinstance(mixer, Mixer): + raise TypeError( + f"Track 'mixer' argument must be a Mixer, received {type(mixer)}" + ) + + _sdl3_mixer_c.Track.__init__(self, mixer) + + def set_audiostream(self, audiostream: audio.AudioStream | None) -> None: + if isinstance(audiostream, audio.AudioStream): + _sdl3_mixer_c.Track.set_audiostream(self, audiostream) + elif audiostream is None: + _sdl3_mixer_c.Track.set_audiostream(self, None) + else: + raise TypeError( + "audiostream argument must be an AudioStream or None, " + f"received {type(audiostream)}" + ) diff --git a/src_py/meson.build b/src_py/meson.build index b85d194f7c..59a098f9b0 100644 --- a/src_py/meson.build +++ b/src_py/meson.build @@ -29,6 +29,7 @@ if not sdl_ttf_dep.found() and freetype_dep.found() endif if sdl_api == 3 + py.install_sources('_sdl3_mixer.py', subdir: pg) py.install_sources('_audio.py', subdir: pg) endif From f2533c20f09bdbb939c68c321ae5a194c0db1464 Mon Sep 17 00:00:00 2001 From: Starbuck5 <46412508+Starbuck5@users.noreply.github.com> Date: Sun, 10 May 2026 21:07:40 -0700 Subject: [PATCH 2/4] Improve sdl3 mixer robustness --- src_c/_sdl3_mixer_c.c | 24 ++++++++++++++++++++---- src_py/_sdl3_mixer.py | 4 +--- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src_c/_sdl3_mixer_c.c b/src_c/_sdl3_mixer_c.c index afa771d02a..98f690d980 100644 --- a/src_c/_sdl3_mixer_c.c +++ b/src_c/_sdl3_mixer_c.c @@ -97,6 +97,9 @@ pg_mixer_obj_play_audio(PGMixerObject *self, PyObject *args, PyObject *kwargs) char *keywords[] = {"audio", NULL}; PyObject *audio_type = PyObject_GetAttrString((PyObject *)self, "_audio_type"); + if (audio_type == NULL) { + return NULL; + } if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O!", keywords, audio_type, &audio)) { @@ -414,6 +417,9 @@ pg_audio_obj_init(PGAudioObject *self, PyObject *args, PyObject *kwargs) char *keywords[] = {"file", "predecode", "preferred_mixer", NULL}; PyObject *mixer_type = PyObject_GetAttrString((PyObject *)self, "_mixer_type"); + if (mixer_type == NULL) { + return -1; + } if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|pO", keywords, &file, &predecode, &mixer_or_none)) { @@ -497,6 +503,9 @@ pg_audio_obj_from_raw(PyTypeObject *cls, PyObject *args, PyObject *kwargs) char *keywords[] = {"buffer", "spec", "preferred_mixer", NULL}; PyObject *mixer_type = PyObject_GetAttrString((PyObject *)cls, "_mixer_type"); + if (mixer_type == NULL) { + return NULL; + } if (!PyArg_ParseTupleAndKeywords(args, kwargs, "OO|O", keywords, &buffer, &spec_obj, &mixer_or_none)) { @@ -547,6 +556,8 @@ pg_audio_obj_from_raw(PyTypeObject *cls, PyObject *args, PyObject *kwargs) return NULL; } + // LoadRawAudio does a copy (unlike LoadRawAudioNoCopy), so it is not + // necessary to keep any references to the buffer. MIX_Audio *raw_audio = MIX_LoadRawAudio(mixer, buf, (size_t)len, &spec); if (raw_audio == NULL) { Py_DECREF(bytes); @@ -569,6 +580,9 @@ pg_audio_obj_from_sine_wave(PyTypeObject *cls, PyObject *args, char *keywords[] = {"hz", "amplitude", "preferred_mixer", "ms", NULL}; PyObject *mixer_type = PyObject_GetAttrString((PyObject *)cls, "_mixer_type"); + if (mixer_type == NULL) { + return NULL; + } if (!PyArg_ParseTupleAndKeywords(args, kwargs, "if|Oi", keywords, &hz, &litude, &mixer_or_none, &ms)) { @@ -591,8 +605,6 @@ pg_audio_obj_from_sine_wave(PyTypeObject *cls, PyObject *args, return NULL; } - // MIX_CreateSineWaveAudio is bugged right now (2025-10-04), - // complains about invalid context parameter. MIX_Audio *sine_wave_audio = MIX_CreateSineWaveAudio(mixer, hz, amplitude, ms); if (sine_wave_audio == NULL) { @@ -686,13 +698,13 @@ pg_audio_obj_get_metadata(PGAudioObject *self, PyObject *_null) total_track_obj = Py_NewRef(Py_None); } - SDL_UnlockProperties(props); - PyObject *meta_dict = Py_BuildValue("{sz sz sz sz sN sN}", "title", title, "artist", artist, "album", album, "copyright", copyright, "track_num", track_obj, "total_tracks", total_track_obj); + SDL_UnlockProperties(props); + return meta_dict; } @@ -872,6 +884,9 @@ pg_track_obj_set_audio(PGTrackObject *self, PyObject *args, PyObject *kwargs) char *keywords[] = {"audio", NULL}; PyObject *audio_type = PyObject_GetAttrString((PyObject *)self, "_audio_type"); + if (audio_type == NULL) { + return NULL; + } if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O", keywords, &audio_or_none)) { @@ -995,6 +1010,7 @@ pg_track_obj_set_filestream(PGTrackObject *self, PyObject *args, } if (!MIX_SetTrackIOStream(self->track, io, true)) { + SDL_CloseIO(io); return RAISE(pgExc_SDLError, SDL_GetError()); } diff --git a/src_py/_sdl3_mixer.py b/src_py/_sdl3_mixer.py index 638e782340..09a8ddd32b 100644 --- a/src_py/_sdl3_mixer.py +++ b/src_py/_sdl3_mixer.py @@ -107,10 +107,8 @@ def __init__(self, mixer: Mixer) -> None: _sdl3_mixer_c.Track.__init__(self, mixer) def set_audiostream(self, audiostream: audio.AudioStream | None) -> None: - if isinstance(audiostream, audio.AudioStream): + if isinstance(audiostream, audio.AudioStream) or audiostream is None: _sdl3_mixer_c.Track.set_audiostream(self, audiostream) - elif audiostream is None: - _sdl3_mixer_c.Track.set_audiostream(self, None) else: raise TypeError( "audiostream argument must be an AudioStream or None, " From df94aa65381fcac7236788b996d3d66ab5e57381 Mon Sep 17 00:00:00 2001 From: Starbuck5 <46412508+Starbuck5@users.noreply.github.com> Date: Sun, 10 May 2026 21:13:37 -0700 Subject: [PATCH 3/4] Re-order sine args for consistency with pref mixer at end --- buildconfig/stubs/pygame/_sdl3_mixer.pyi | 2 +- src_c/_sdl3_mixer_c.c | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/buildconfig/stubs/pygame/_sdl3_mixer.pyi b/buildconfig/stubs/pygame/_sdl3_mixer.pyi index 0b1ee448f0..42839a9693 100644 --- a/buildconfig/stubs/pygame/_sdl3_mixer.pyi +++ b/buildconfig/stubs/pygame/_sdl3_mixer.pyi @@ -80,8 +80,8 @@ class Audio: cls, hz: int, amplitude: float, - preferred_mixer: Mixer | None = None, ms: int = -1, + preferred_mixer: Mixer | None = None, ) -> Audio: ... @property def duration_frames(self) -> int | None: ... diff --git a/src_c/_sdl3_mixer_c.c b/src_c/_sdl3_mixer_c.c index 98f690d980..89ccf333f1 100644 --- a/src_c/_sdl3_mixer_c.c +++ b/src_c/_sdl3_mixer_c.c @@ -577,15 +577,15 @@ pg_audio_obj_from_sine_wave(PyTypeObject *cls, PyObject *args, int hz, ms = -1; float amplitude; PyObject *mixer_or_none = Py_None; - char *keywords[] = {"hz", "amplitude", "preferred_mixer", "ms", NULL}; + char *keywords[] = {"hz", "amplitude", "ms", "preferred_mixer", NULL}; PyObject *mixer_type = PyObject_GetAttrString((PyObject *)cls, "_mixer_type"); if (mixer_type == NULL) { return NULL; } - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "if|Oi", keywords, &hz, - &litude, &mixer_or_none, &ms)) { + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "if|iO", keywords, &hz, + &litude, &ms, &mixer_or_none)) { Py_DECREF(mixer_type); return NULL; } From 1435f2056dca17ac86bdd54e9acfae4d8054dfff Mon Sep 17 00:00:00 2001 From: Starbuck5 <46412508+Starbuck5@users.noreply.github.com> Date: Sun, 10 May 2026 21:15:38 -0700 Subject: [PATCH 4/4] Use Py_NewRef in _sdl3_mixer_c --- src_c/_sdl3_mixer_c.c | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src_c/_sdl3_mixer_c.c b/src_c/_sdl3_mixer_c.c index 89ccf333f1..d530f48787 100644 --- a/src_c/_sdl3_mixer_c.c +++ b/src_c/_sdl3_mixer_c.c @@ -788,8 +788,7 @@ pg_track_obj_init(PGTrackObject *self, PyObject *args, PyObject *kwargs) // Mixers own Tracks. When the Mixer is deallocated, the tracks become // invalid. So we need to hold a reference to prevent Mixer deallocating // before any of the Tracks it owns. - Py_INCREF(mixer); - self->mixer_obj = (PyObject *)mixer; + self->mixer_obj = Py_NewRef(mixer); return 0; } @@ -811,8 +810,7 @@ pg_track_obj_dealloc(PGTrackObject *self) static PyObject * pg_track_obj_get_mixer(PGTrackObject *self, PyObject *_null) { - Py_INCREF(self->mixer_obj); - return self->mixer_obj; + return Py_NewRef(self->mixer_obj); } static PyObject * @@ -914,8 +912,7 @@ pg_track_obj_set_audio(PGTrackObject *self, PyObject *args, PyObject *kwargs) if (audio != NULL) { // We've successfully added an audio object, yay! - Py_INCREF(audio_or_none); - self->source_obj = audio_or_none; + self->source_obj = Py_NewRef(audio_or_none); } Py_RETURN_NONE; @@ -927,8 +924,7 @@ pg_track_obj_get_audio(PGTrackObject *self, PyObject *_null) if (MIX_GetTrackAudio(self->track) != NULL) { // This track object owns an audio, therefore our source object must // be non-null, and an audio object. - Py_INCREF(self->source_obj); - return self->source_obj; + return Py_NewRef(self->source_obj); } Py_RETURN_NONE; @@ -973,8 +969,7 @@ pg_track_obj_set_audiostream(PGTrackObject *self, PyObject *args, if (stream != NULL) { // We've successfully added an audio object, yay! - Py_INCREF(audiostream_or_none); - self->source_obj = audiostream_or_none; + self->source_obj = Py_NewRef(audiostream_or_none); } Py_RETURN_NONE; @@ -986,8 +981,7 @@ pg_track_obj_get_audiostream(PGTrackObject *self, PyObject *_null) if (MIX_GetTrackAudioStream(self->track) != NULL) { // This track object owns an audio, therefore our source object must // be non-null, and an audio object. - Py_INCREF(self->source_obj); - return self->source_obj; + return Py_NewRef(self->source_obj); } Py_RETURN_NONE; @@ -1022,8 +1016,7 @@ pg_track_obj_set_filestream(PGTrackObject *self, PyObject *args, // Theoretically this is keeping Python file object (like BytesIO) alive // through the stream, but maybe the rwObject subsystem is smart enough // to do that. - Py_INCREF(file_obj); - self->source_obj = file_obj; + self->source_obj = Py_NewRef(file_obj); Py_RETURN_NONE; }