From 6e99e2f6f98a1aea5d2c36fa07e4c1325424a30d Mon Sep 17 00:00:00 2001 From: Starbuck5 <46412508+Starbuck5@users.noreply.github.com> Date: Tue, 9 Dec 2025 00:06:50 -0800 Subject: [PATCH 01/10] SDL3 _audio module --- buildconfig/stubs/meson.build | 9 +- buildconfig/stubs/mypy_allow_list.txt | 3 + buildconfig/stubs/pygame/_audio.pyi | 164 ++++ src_c/_base_audio.c | 1137 +++++++++++++++++++++++++ src_c/meson.build | 15 +- src_py/_audio.py | 444 ++++++++++ src_py/meson.build | 4 + 7 files changed, 1773 insertions(+), 3 deletions(-) create mode 100644 buildconfig/stubs/pygame/_audio.pyi create mode 100644 src_c/_base_audio.c create mode 100644 src_py/_audio.py diff --git a/buildconfig/stubs/meson.build b/buildconfig/stubs/meson.build index b20178f4a8..ba219e115c 100644 --- a/buildconfig/stubs/meson.build +++ b/buildconfig/stubs/meson.build @@ -1,6 +1,13 @@ +pg_stub_excludes = ['.flake8'] + +# SDL3 only! +if sdl_api != 3 + pg_stub_excludes += ['_audio.pyi'] +endif + install_subdir( 'pygame', - exclude_files: '.flake8', + exclude_files: pg_stub_excludes, install_dir: pg_dir, strip_directory: true, install_tag: 'pg-tag', diff --git a/buildconfig/stubs/mypy_allow_list.txt b/buildconfig/stubs/mypy_allow_list.txt index 1e7c532d72..63c2b96127 100644 --- a/buildconfig/stubs/mypy_allow_list.txt +++ b/buildconfig/stubs/mypy_allow_list.txt @@ -27,3 +27,6 @@ pygame\.pypm pygame\._sdl2\.mixer pygame\.sysfont.* pygame\.docs.* + +# Remove me when we're checking stubs for SDL3! +pygame\._audio diff --git a/buildconfig/stubs/pygame/_audio.pyi b/buildconfig/stubs/pygame/_audio.pyi new file mode 100644 index 0000000000..e0c0854fb5 --- /dev/null +++ b/buildconfig/stubs/pygame/_audio.pyi @@ -0,0 +1,164 @@ +from collections.abc import Callable +from typing import TypeVar + +from pygame.typing import FileLike +from typing_extensions import Buffer + +# TODO: Support SDL3 stubchecking without failing when on SDL2 builds +# Right now this module is unconditionally skipped in mypy_allow_list.txt + +def init() -> None: ... + +# def quit() -> None: ... +def get_init() -> bool: ... +def get_current_driver() -> str: ... +def get_drivers() -> list[str]: ... +def get_playback_devices() -> list[AudioDevice]: ... +def get_recording_devices() -> list[AudioDevice]: ... + +# def mix_audio(dst: Buffer, src: Buffer, format: AudioFormat, volume: float) -> None: ... +def load_wav(file: FileLike) -> tuple[AudioSpec, bytes]: ... + +# def convert_samples( +# src_spec: AudioSpec, src_data: Buffer, dst_spec: AudioSpec +# ) -> bytes: ... + +DEFAULT_PLAYBACK_DEVICE: AudioDevice +DEFAULT_RECORDING_DEVICE: AudioDevice + +# T = TypeVar("T") +# stream_callback = Callable[[T, AudioStream, int, int], None] +# post_mix_callback = Callable[[T, AudioStream, Buffer], None] +# iteration_callback = Callable[[T, AudioDevice, bool], None] + +class AudioFormat: + @property + def bitsize(self) -> int: ... + @property + def bytesize(self) -> int: ... + @property + def is_float(self) -> bool: ... + @property + def is_int(self) -> bool: ... + @property + def is_big_endian(self) -> bool: ... + @property + def is_little_endian(self) -> bool: ... + @property + def is_signed(self) -> bool: ... + @property + def is_unsigned(self) -> bool: ... + @property + def name(self) -> str: ... + @property + def silence_value(self) -> bytes: ... + def __index__(self) -> int: ... + def __repr__(self) -> str: ... + +UNKNOWN: AudioFormat +U8: AudioFormat +S8: AudioFormat +S16LE: AudioFormat +S16BE: AudioFormat +S32LE: AudioFormat +S32BE: AudioFormat +F32LE: AudioFormat +F32BE: AudioFormat +S16: AudioFormat +S32: AudioFormat +F32: AudioFormat + +class AudioSpec: + def __init__(self, format: AudioFormat, channels: int, frequency: int) -> None: ... + @property + def format(self) -> AudioFormat: ... + @property + def channels(self) -> int: ... + @property + def frequency(self) -> int: ... + @property + def framesize(self) -> int: ... + def __repr__(self) -> str: ... + +class AudioDevice: + def open(self, spec: AudioSpec | None = None) -> LogicalAudioDevice: ... + # def open_stream( + # self, + # spec: AudioSpec | None, + # callback: stream_callback | None, + # userdata: T | None, + # ) -> AudioStream: ... + @property + def is_playback(self) -> bool: ... + @property + def name(self) -> str: ... + # Need something for https://wiki.libsdl.org/SDL3/SDL_GetAudioDeviceFormat + @property + def channel_map(self) -> list[int] | None: ... + +class LogicalAudioDevice(AudioDevice): + def pause(self) -> None: ... + def resume(self) -> None: ... + @property + def paused(self) -> bool: ... + @property + def gain(self) -> float: ... + @gain.setter + def gain(self, value: float) -> None: ... + # def set_iteration_callbacks( + # self, + # start: iteration_callback | None, + # end: iteration_callback | None, + # userdata: T, + # ) -> None: ... + # def set_post_mix_callback( + # self, callback: post_mix_callback | None, userdata: T + # ) -> None: ... + +class AudioStream: + def __init__(self, src_spec: AudioSpec, dst_spec: AudioSpec) -> None: ... + def bind(self, device: LogicalAudioDevice) -> None: ... + def unbind(self) -> None: ... + def clear(self) -> None: ... + def flush(self) -> None: ... + @property + def num_available_bytes(self) -> int: ... + @property + def num_queued_bytes(self) -> int: ... + def get_data(self, size: int) -> bytes: ... + def put_data(self, data: Buffer) -> None: ... + def pause_device(self) -> None: ... + def resume_device(self) -> None: ... + @property + def device_paused(self) -> bool: ... + @property + def device(self) -> LogicalAudioDevice | None: ... + @property + def src_spec(self) -> AudioSpec: ... + @src_spec.setter + def src_spec(self, value: AudioSpec) -> None: ... + @property + def dst_spec(self) -> AudioSpec: ... + @dst_spec.setter + def dst_spec(self, value: AudioSpec) -> None: ... + @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_input_channel_map(self, channel_map: list[int] | None) -> None: ... + # def get_input_channel_map(self) -> list[int] | None: ... + # def set_output_channel_map(self, channel_map: list[int] | None) -> None: ... + # def get_output_channel_map(self) -> list[int] | None: ... + def lock(self) -> None: ... + def unlock(self) -> None: ... + # def set_get_callback( + # self, callback: stream_callback | None, userdata: T + # ) -> None: ... + # def set_put_callback( + # self, callback: stream_callback | None, userdata: T + # ) -> None: ... + def __repr__(self) -> str: ... diff --git a/src_c/_base_audio.c b/src_c/_base_audio.c new file mode 100644 index 0000000000..3ba2b45496 --- /dev/null +++ b/src_c/_base_audio.c @@ -0,0 +1,1137 @@ +#include "pygame.h" +#include "pgcompat.h" + +// Useful heap type example @ +// https://github.com/python/cpython/blob/main/Modules/xxlimited.c + +// *************************************************************************** +// OVERALL DEFINITIONS +// *************************************************************************** + +typedef struct { + bool audio_initialized; + PyObject *audio_device_state_type; + PyObject *audio_stream_state_type; +} audio_state; + +#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"); \ + } + +// *************************************************************************** +// AUDIO.AUDIODEVICE CLASS +// *************************************************************************** + +// The documentation says heap types need to support GC, so we're implementing +// traverse even though the object has no explicit references. +static int +adevice_state_traverse(PyObject *op, visitproc visit, void *arg) +{ + // Visit the type + Py_VISIT(Py_TYPE(op)); + return 0; +} + +static void +adevice_state_dealloc(PGAudioDeviceStateObject *self) +{ + // Only close devices that have been opened. + // (logical devices, not physical) + if (!SDL_IsAudioDevicePhysical(self->devid)) { + SDL_CloseAudioDevice(self->devid); + } + PyObject_GC_UnTrack(self); + PyTypeObject *tp = Py_TYPE(self); + freefunc free = PyType_GetSlot(tp, Py_tp_free); + free(self); + Py_DECREF(tp); +} + +static PyMemberDef adevice_state_members[] = { + {"id", Py_T_UINT, offsetof(PGAudioDeviceStateObject, devid), Py_READONLY, + NULL}, + {NULL} /* Sentinel */ +}; + +static PyType_Slot adevice_state_slots[] = { + {Py_tp_members, adevice_state_members}, + {Py_tp_traverse, adevice_state_traverse}, + {Py_tp_dealloc, adevice_state_dealloc}, + {0, NULL}}; + +static PyType_Spec adevice_state_spec = { + .name = "AudioDeviceState", + .basicsize = sizeof(PGAudioDeviceStateObject), + .itemsize = 0, + .flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, + .slots = adevice_state_slots}; + +static PyObject * +pg_audio_is_audio_device_playback(PyObject *module, PyObject *arg) +{ + // SDL_IsAudioDevicePlayback + // arg: PGAudioDeviceStateObject + + SDL_AudioDeviceID devid = ((PGAudioDeviceStateObject *)arg)->devid; + if (SDL_IsAudioDevicePlayback(devid)) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; +} + +static PyObject * +pg_audio_get_audio_device_name(PyObject *module, PyObject *const *args, + Py_ssize_t nargs) +{ + // assert nargs == 1 + // assert type(args[0]) == AudioDeviceState + SDL_AudioDeviceID devid = ((PGAudioDeviceStateObject *)args[0])->devid; + const char *name = SDL_GetAudioDeviceName(devid); + if (name == NULL) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + return PyUnicode_FromString(name); +} + +static PyObject * +pg_audio_get_audio_device_channel_map(PyObject *module, PyObject *arg) +{ + // SDL_GetAudioDeviceChannelMap + // arg: PGAudioDeviceStateObject + + SDL_AudioDeviceID devid = ((PGAudioDeviceStateObject *)arg)->devid; + + int count; + int *channel_map = SDL_GetAudioDeviceChannelMap(devid, &count); + if (channel_map == NULL) { + Py_RETURN_NONE; + } + + PyObject *channel_map_list = PyList_New(count); + if (channel_map_list == NULL) { + SDL_free(channel_map); + return NULL; + } + PyObject *item; + for (int i = 0; i < count; i++) { + item = PyLong_FromLong(channel_map[i]); + if (item == NULL) { + SDL_free(channel_map); + Py_DECREF(channel_map_list); + return NULL; + } + if (PyList_SetItem(channel_map_list, i, item) < 0) { + SDL_free(channel_map); + Py_DECREF(item); + Py_DECREF(channel_map_list); + return NULL; + } + } + + SDL_free(channel_map); + return channel_map_list; +} + +static PyObject * +pg_audio_pause_audio_device(PyObject *module, PyObject *arg) +{ + // SDL_PauseAudioDevice + // arg: PGAudioDeviceStateObject + + SDL_AudioDeviceID devid = ((PGAudioDeviceStateObject *)arg)->devid; + if (!SDL_PauseAudioDevice(devid)) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + Py_RETURN_NONE; +} + +static PyObject * +pg_audio_resume_audio_device(PyObject *module, PyObject *arg) +{ + // SDL_ResumeAudioDevice + // arg: PGAudioDeviceStateObject + + SDL_AudioDeviceID devid = ((PGAudioDeviceStateObject *)arg)->devid; + if (!SDL_ResumeAudioDevice(devid)) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + Py_RETURN_NONE; +} + +static PyObject * +pg_audio_audio_device_paused(PyObject *module, PyObject *arg) +{ + // SDL_AudioDevicePaused + // arg: PGAudioDeviceStateObject + + SDL_AudioDeviceID devid = ((PGAudioDeviceStateObject *)arg)->devid; + if (SDL_AudioDevicePaused(devid)) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; +} + +static PyObject * +pg_audio_get_audio_device_gain(PyObject *module, PyObject *arg) +{ + // SDL_GetAudioDeviceGain + // arg: PGAudioDeviceStateObject + + SDL_AudioDeviceID devid = ((PGAudioDeviceStateObject *)arg)->devid; + float gain = SDL_GetAudioDeviceGain(devid); + if (gain == -1.0f) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + return PyFloat_FromDouble((double)gain); +} + +static PyObject * +pg_audio_set_audio_device_gain(PyObject *module, PyObject *const *args, + Py_ssize_t nargs) +{ + // SDL_SetAudioDeviceGain + // arg0: PGAudioDeviceStateObject, gain: float + + SDL_AudioDeviceID devid = ((PGAudioDeviceStateObject *)args[0])->devid; + double gain = PyFloat_AsDouble(args[1]); + if (gain == -1.0 && PyErr_Occurred()) { + return NULL; + } + + if (!SDL_SetAudioDeviceGain(devid, (float)gain)) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + Py_RETURN_NONE; +} + +static PyObject * +pg_audio_open_audio_device(PyObject *module, PyObject *const *args, + Py_ssize_t nargs) +{ + // SDL_OpenAudioDevice + // arg0: PGAudioDeviceStateObject, format: int | unset, channels: int | + // unset, frequency: int | unset + + audio_state *state = GET_STATE(module); + PyTypeObject *adevice_state_type = + (PyTypeObject *)state->audio_device_state_type; + + SDL_AudioDeviceID devid = ((PGAudioDeviceStateObject *)args[0])->devid; + + SDL_AudioSpec *spec_p = NULL; + SDL_AudioSpec spec; + if (nargs != 1) { + spec.format = PyLong_AsInt(args[1]); + spec.channels = PyLong_AsInt(args[2]); + spec.freq = PyLong_AsInt(args[3]); + + // Check that they all succeeded + if (spec.format == -1 || spec.channels == -1 || spec.freq == -1) { + if (PyErr_Occurred()) { + return NULL; + } + } + + spec_p = &spec; + } + + SDL_AudioDeviceID logical_id = SDL_OpenAudioDevice(devid, spec_p); + if (logical_id == 0) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + PGAudioDeviceStateObject *device = + (PGAudioDeviceStateObject *)adevice_state_type->tp_alloc( + adevice_state_type, 0); + if (device == NULL) { + SDL_CloseAudioDevice(logical_id); + return NULL; + } + device->devid = logical_id; + + return (PyObject *)device; +} + +// *************************************************************************** +// AUDIO.AUDIOSTREAM CLASS +// *************************************************************************** + +// The documentation says heap types need to support GC, so we're implementing +// traverse even though the object has no explicit references. +static int +astream_state_traverse(PyObject *op, visitproc visit, void *arg) +{ + // Visit the type + Py_VISIT(Py_TYPE(op)); + return 0; +} + +static void +astream_state_dealloc(PGAudioStreamStateObject *self) +{ + SDL_DestroyAudioStream(self->stream); + PyObject_GC_UnTrack(self); + PyTypeObject *tp = Py_TYPE(self); + freefunc free = PyType_GetSlot(tp, Py_tp_free); + free(self); + Py_DECREF(tp); +} + +static PyType_Slot astream_state_slots[] = { + {Py_tp_traverse, astream_state_traverse}, + {Py_tp_dealloc, astream_state_dealloc}, + {0, NULL}}; + +static PyType_Spec astream_state_spec = { + .name = "AudioStreamState", + .basicsize = sizeof(PGAudioStreamStateObject), + .itemsize = 0, + .flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, + .slots = astream_state_slots}; + +static PyObject * +pg_audio_create_audio_stream(PyObject *module, PyObject *const *args, + Py_ssize_t nargs) +{ + // SDL_CreateAudioStream + // src_format: int, src_channels: int, src_frequency: int, + // dst_format: int, dst_channels: int, dst_frequency: int + + audio_state *state = GET_STATE(module); + PyTypeObject *astream_state_type = + (PyTypeObject *)state->audio_stream_state_type; + + SDL_AudioSpec src, dst; + + src.format = PyLong_AsInt(args[0]); + src.channels = PyLong_AsInt(args[1]); + src.freq = PyLong_AsInt(args[2]); + dst.format = PyLong_AsInt(args[3]); + dst.channels = PyLong_AsInt(args[4]); + dst.freq = PyLong_AsInt(args[5]); + + // Check that they all succeeded + if (src.format == -1 || src.channels == -1 || src.freq == -1 || + dst.format == -1 || dst.channels == -1 || dst.freq == -1) { + if (PyErr_Occurred()) { + return NULL; + } + } + + SDL_AudioStream *stream = SDL_CreateAudioStream(&src, &dst); + if (stream == NULL) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + PGAudioStreamStateObject *stream_state = + (PGAudioStreamStateObject *)astream_state_type->tp_alloc( + astream_state_type, 0); + + if (stream_state == NULL) { + SDL_DestroyAudioStream(stream); + return NULL; + } + stream_state->stream = stream; + + return (PyObject *)stream_state; +} + +static PyObject * +pg_audio_bind_audio_stream(PyObject *module, PyObject *const *args, + Py_ssize_t nargs) +{ + // SDL_BindAudioStream + // arg0: PGAudioDeviceStateObject, arg1: PGAudioStreamStateObject + + SDL_AudioDeviceID devid = ((PGAudioDeviceStateObject *)args[0])->devid; + SDL_AudioStream *stream = ((PGAudioStreamStateObject *)args[1])->stream; + + if (!SDL_BindAudioStream(devid, stream)) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + Py_RETURN_NONE; +} + +static PyObject * +pg_audio_unbind_audio_stream(PyObject *module, PyObject *arg) +{ + // SDL_UnbindAudioStream + // arg: PGAudioStreamStateObject + + SDL_AudioStream *stream = ((PGAudioStreamStateObject *)arg)->stream; + SDL_UnbindAudioStream(stream); + Py_RETURN_NONE; +} + +static PyObject * +pg_audio_clear_audio_stream(PyObject *module, PyObject *arg) +{ + // SDL_ClearAudioStream + // arg: PGAudioStreamStateObject + + SDL_AudioStream *stream = ((PGAudioStreamStateObject *)arg)->stream; + + if (!SDL_ClearAudioStream(stream)) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + Py_RETURN_NONE; +} + +static PyObject * +pg_audio_flush_audio_stream(PyObject *module, PyObject *arg) +{ + // SDL_FlushAudioStream + // arg: PGAudioStreamStateObject + + SDL_AudioStream *stream = ((PGAudioStreamStateObject *)arg)->stream; + + if (!SDL_FlushAudioStream(stream)) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + Py_RETURN_NONE; +} + +static PyObject * +pg_audio_get_audio_stream_available(PyObject *module, PyObject *arg) +{ + // SDL_GetAudioStreamAvailable + // arg: PGAudioStreamStateObject + + SDL_AudioStream *stream = ((PGAudioStreamStateObject *)arg)->stream; + + int available = SDL_GetAudioStreamAvailable(stream); + if (available == -1) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + return PyLong_FromLong(available); +} + +static PyObject * +pg_audio_get_audio_stream_queued(PyObject *module, PyObject *arg) +{ + // SDL_GetAudioStreamQueued + // arg: PGAudioStreamStateObject + + SDL_AudioStream *stream = ((PGAudioStreamStateObject *)arg)->stream; + + int queued = SDL_GetAudioStreamQueued(stream); + if (queued == -1) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + return PyLong_FromLong(queued); +} + +static PyObject * +pg_audio_get_audio_stream_data(PyObject *module, PyObject *const *args, + Py_ssize_t nargs) +{ + // SDL_GetAudioStreamData + // stream_state: PGAudioStreamStateObject, size: int + + SDL_AudioStream *stream = ((PGAudioStreamStateObject *)args[0])->stream; + + int size = PyLong_AsInt(args[1]); + if (size == -1 && PyErr_Occurred()) { + return NULL; + } + + void *buf = malloc(size); + if (buf == NULL) { + return PyErr_NoMemory(); + } + + int bytes_read = SDL_GetAudioStreamData(stream, buf, size); + + if (bytes_read == -1) { + free(buf); + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + PyObject *bytes = PyBytes_FromStringAndSize(buf, bytes_read); + free(buf); + if (bytes == NULL) { + return NULL; + } + + return bytes; +} + +static PyObject * +pg_audio_put_audio_stream_data(PyObject *module, PyObject *const *args, + Py_ssize_t nargs) +{ + // SDL_PutAudioStreamData + // stream_state: PGAudioStreamStateObject, data: Buffer + + SDL_AudioStream *stream = ((PGAudioStreamStateObject *)args[0])->stream; + + PyObject *bytes = PyBytes_FromObject(args[1]); + if (bytes == NULL) { + return NULL; + } + + void *buf; + Py_ssize_t len; + + if (PyBytes_AsStringAndSize(bytes, (char **)&buf, &len) != 0) { + Py_DECREF(bytes); + return NULL; + } + + if (len > INT_MAX) { + Py_DECREF(bytes); + return RAISE(pgExc_SDLError, "audio buffer too large"); + } + + if (!SDL_PutAudioStreamData(stream, buf, (int)len)) { + Py_DECREF(bytes); + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + Py_DECREF(bytes); + Py_RETURN_NONE; +} + +static PyObject * +pg_audio_pause_audio_stream_device(PyObject *module, PyObject *arg) +{ + // SDL_PauseAudioStreamDevice + // arg: PGAudioStreamStateObject + + SDL_AudioStream *stream = ((PGAudioStreamStateObject *)arg)->stream; + if (!SDL_PauseAudioStreamDevice(stream)) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + Py_RETURN_NONE; +} + +static PyObject * +pg_audio_resume_audio_stream_device(PyObject *module, PyObject *arg) +{ + // SDL_ResumeAudioStreamDevice + // arg: PGAudioStreamStateObject + + SDL_AudioStream *stream = ((PGAudioStreamStateObject *)arg)->stream; + if (!SDL_ResumeAudioStreamDevice(stream)) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + Py_RETURN_NONE; +} + +static PyObject * +pg_audio_audio_stream_device_paused(PyObject *module, PyObject *arg) +{ + // SDL_AudioStreamDevicePaused + // arg: PGAudioStreamStateObject + + SDL_AudioStream *stream = ((PGAudioStreamStateObject *)arg)->stream; + if (SDL_AudioStreamDevicePaused(stream)) { + Py_RETURN_TRUE; + } + + Py_RETURN_FALSE; +} + +static PyObject * +pg_audio_get_audio_stream_format(PyObject *module, PyObject *arg) +{ + // SDL_GetAudioStreamFormat + // arg: PGAudioStreamStateObject + + SDL_AudioStream *stream = ((PGAudioStreamStateObject *)arg)->stream; + SDL_AudioSpec src_spec, dst_spec; + + if (!SDL_GetAudioStreamFormat(stream, &src_spec, &dst_spec)) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + return Py_BuildValue("iiiiii", src_spec.format, src_spec.channels, + src_spec.freq, dst_spec.format, dst_spec.channels, + dst_spec.freq); +} + +static PyObject * +pg_audio_set_audio_stream_format(PyObject *module, PyObject *const *args, + Py_ssize_t nargs) +{ + // SDL_SetAudioStreamFormat + // arg0: PGAudioStreamStateObject, + // src format: (format int, channels int, frequency int) | None + // dst format: (format int, channels int, frequency int) | None + + SDL_AudioSpec src, dst; + SDL_AudioSpec *src_p = NULL; + SDL_AudioSpec *dst_p = NULL; + + SDL_AudioStream *stream = ((PGAudioStreamStateObject *)args[0])->stream; + + 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)); + src_p = &src; + if ((src.format == -1 || src.channels == -1 || src.freq == -1) && + PyErr_Occurred()) { + return NULL; + } + } + if (!Py_IsNone(args[2])) { + dst.format = PyLong_AsInt(PyTuple_GetItem(args[2], 0)); + dst.channels = PyLong_AsInt(PyTuple_GetItem(args[2], 1)); + dst.freq = PyLong_AsInt(PyTuple_GetItem(args[2], 2)); + dst_p = &dst; + if ((dst.format == -1 || dst.channels == -1 || dst.freq == -1) && + PyErr_Occurred()) { + return NULL; + } + } + + if (!SDL_SetAudioStreamFormat(stream, src_p, dst_p)) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + Py_RETURN_NONE; +} + +static PyObject * +pg_audio_get_audio_stream_gain(PyObject *module, PyObject *arg) +{ + // SDL_GetAudioStreamGain + // arg: PGAudioStreamStateObject + + SDL_AudioStream *stream = ((PGAudioStreamStateObject *)arg)->stream; + float gain = SDL_GetAudioStreamGain(stream); + + if (gain == -1.0f) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + return PyFloat_FromDouble((double)gain); +} + +static PyObject * +pg_audio_set_audio_stream_gain(PyObject *module, PyObject *const *args, + Py_ssize_t nargs) +{ + // SDL_SetAudioStreamGain + // arg0: PGAudioStreamStateObject, gain: float + + SDL_AudioStream *stream = ((PGAudioStreamStateObject *)args[0])->stream; + + double gain = PyFloat_AsDouble(args[1]); + if (gain == -1.0 && PyErr_Occurred()) { + return NULL; + } + + if (!SDL_SetAudioStreamGain(stream, (float)gain)) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + Py_RETURN_NONE; +} + +static PyObject * +pg_audio_get_audio_stream_frequency_ratio(PyObject *module, PyObject *arg) +{ + // SDL_GetAudioStreamFrequencyRatio + // arg: PGAudioStreamStateObject + + SDL_AudioStream *stream = ((PGAudioStreamStateObject *)arg)->stream; + float frequency_ratio = SDL_GetAudioStreamFrequencyRatio(stream); + + if (frequency_ratio == 0.0f) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + return PyFloat_FromDouble((double)frequency_ratio); +} + +static PyObject * +pg_audio_set_audio_stream_frequency_ratio(PyObject *module, + PyObject *const *args, + Py_ssize_t nargs) +{ + // SDL_SetAudioStreamFrequencyRatio + // arg0: PGAudioStreamStateObject, frequency_ratio: float + + SDL_AudioStream *stream = ((PGAudioStreamStateObject *)args[0])->stream; + + double frequency_ratio = PyFloat_AsDouble(args[1]); + if (frequency_ratio == -1.0 && PyErr_Occurred()) { + return NULL; + } + + if (!SDL_SetAudioStreamFrequencyRatio(stream, (float)frequency_ratio)) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + Py_RETURN_NONE; +} + +static PyObject * +pg_audio_lock_audio_stream(PyObject *module, PyObject *arg) +{ + // SDL_LockAudioStream + // arg: PGAudioStreamStateObject + + SDL_AudioStream *stream = ((PGAudioStreamStateObject *)arg)->stream; + if (!SDL_LockAudioStream(stream)) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + Py_RETURN_NONE; +} + +static PyObject * +pg_audio_unlock_audio_stream(PyObject *module, PyObject *arg) +{ + // SDL_UnlockAudioStream + // arg: PGAudioStreamStateObject + + SDL_AudioStream *stream = ((PGAudioStreamStateObject *)arg)->stream; + if (!SDL_UnlockAudioStream(stream)) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + Py_RETURN_NONE; +} + +// *************************************************************************** +// MODULE METHODS +// *************************************************************************** + +static PyObject * +pg_audio_init(PyObject *module, PyObject *_null) +{ + audio_state *state = GET_STATE(module); + if (!state->audio_initialized) { + if (!SDL_InitSubSystem(SDL_INIT_AUDIO)) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + state->audio_initialized = true; + } + Py_RETURN_NONE; +} + +static PyObject * +pg_audio_quit(PyObject *module, PyObject *_null) +{ + audio_state *state = GET_STATE(module); + if (state->audio_initialized) { + SDL_QuitSubSystem(SDL_INIT_AUDIO); + state->audio_initialized = false; + } + Py_RETURN_NONE; +} + +static PyObject * +pg_audio_get_init(PyObject *module, PyObject *_null) +{ + // Returns whether the subsystem is initialized, not + // whether _base_audio.init was called! + // EX: mixer would initialize SDL audio subsystem too. + + if (!SDL_WasInit(SDL_INIT_AUDIO)) { + Py_RETURN_FALSE; + } + Py_RETURN_TRUE; +} + +static PyObject * +pg_audio_get_current_driver(PyObject *module, PyObject *_null) +{ + AUDIO_INIT_CHECK(module); + + const char *driver = SDL_GetCurrentAudioDriver(); + if (driver != NULL) { + return PyUnicode_FromString(driver); + } + return RAISE(pgExc_SDLError, SDL_GetError()); +} + +static PyObject * +pg_audio_get_drivers(PyObject *module, PyObject *_null) +{ + int num_drivers = SDL_GetNumAudioDrivers(); + + PyObject *driver_list = PyList_New(num_drivers); + if (driver_list == NULL) { + return NULL; + } + PyObject *item; + const char *driver; + for (int i = 0; i < num_drivers; i++) { + driver = SDL_GetAudioDriver(i); + if (driver == NULL) { + Py_DECREF(driver_list); + return RAISE(pgExc_SDLError, SDL_GetError()); + } + item = PyUnicode_FromString(driver); + if (item == NULL) { + Py_DECREF(driver_list); + return NULL; + } + if (PyList_SetItem(driver_list, i, item) < 0) { + Py_DECREF(item); + Py_DECREF(driver_list); + return NULL; + } + } + + return driver_list; +} + +// Returns Python list of DeviceState objects, or NULL with error set. +static PyObject * +_pg_audio_device_array_to_pylist(SDL_AudioDeviceID *devices, int num_devices, + PyTypeObject *adevice_state_type) +{ + if (devices == NULL) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + PyObject *device_list = PyList_New(num_devices); + if (device_list == NULL) { + return NULL; + } + PGAudioDeviceStateObject *device; + for (int i = 0; i < num_devices; i++) { + device = (PGAudioDeviceStateObject *)adevice_state_type->tp_alloc( + adevice_state_type, 0); + if (device == NULL) { + Py_DECREF(device_list); + return NULL; + } + device->devid = devices[i]; + if (PyList_SetItem(device_list, i, (PyObject *)device) < 0) { + Py_DECREF(device); + Py_DECREF(device_list); + return NULL; + } + } + + return device_list; +} + +static PyObject * +pg_audio_get_playback_device_states(PyObject *module, PyObject *_null) +{ + audio_state *state = GET_STATE(module); + PyTypeObject *adevice_state_type = + (PyTypeObject *)state->audio_device_state_type; + + int num_devices; + SDL_AudioDeviceID *devices = SDL_GetAudioPlaybackDevices(&num_devices); + + PyObject *dev_list = _pg_audio_device_array_to_pylist(devices, num_devices, + adevice_state_type); + SDL_free(devices); + return dev_list; // Fine if NULL, error already set. +} + +static PyObject * +pg_audio_get_recording_device_states(PyObject *module, PyObject *_null) +{ + audio_state *state = GET_STATE(module); + PyTypeObject *adevice_state_type = + (PyTypeObject *)state->audio_device_state_type; + + int num_devices; + SDL_AudioDeviceID *devices = SDL_GetAudioRecordingDevices(&num_devices); + + PyObject *dev_list = _pg_audio_device_array_to_pylist(devices, num_devices, + adevice_state_type); + SDL_free(devices); + return dev_list; // Fine if NULL, error already set. +} + +static PyObject * +pg_audio_load_wav(PyObject *module, PyObject *arg) +{ + // SDL_LoadWAV_IO + // arg: FileLike + + SDL_IOStream *src = pgRWops_FromObject(arg, NULL); + if (src == NULL) { + return NULL; + } + + SDL_AudioSpec spec; + Uint8 *audio_buf; + Uint32 audio_len; + + if (!SDL_LoadWAV_IO(src, true, &spec, &audio_buf, &audio_len)) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + PyObject *bytes = PyBytes_FromStringAndSize((char *)audio_buf, audio_len); + SDL_free(audio_buf); + if (bytes == NULL) { + return NULL; + } + + return Py_BuildValue("Niii", bytes, spec.format, spec.channels, spec.freq); +} + +static PyObject * +pg_audio_get_default_playback_device_state(PyObject *module, PyObject *_null) +{ + audio_state *state = GET_STATE(module); + PyTypeObject *adevice_state_type = + (PyTypeObject *)state->audio_device_state_type; + + PGAudioDeviceStateObject *device = + (PGAudioDeviceStateObject *)adevice_state_type->tp_alloc( + adevice_state_type, 0); + if (device == NULL) { + return NULL; + } + device->devid = SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK; + + return (PyObject *)device; +} + +static PyObject * +pg_audio_get_default_recording_device_state(PyObject *module, PyObject *_null) +{ + audio_state *state = GET_STATE(module); + PyTypeObject *adevice_state_type = + (PyTypeObject *)state->audio_device_state_type; + + PGAudioDeviceStateObject *device = + (PGAudioDeviceStateObject *)adevice_state_type->tp_alloc( + adevice_state_type, 0); + if (device == NULL) { + return NULL; + } + device->devid = SDL_AUDIO_DEVICE_DEFAULT_RECORDING; + + return (PyObject *)device; +} + +static PyObject * +pg_audio_get_silence_value_for_format(PyObject *module, PyObject *const *args, + Py_ssize_t nargs) +{ + // SDL_GetSilenceValueForFormat + // format: int + + int format_num = PyLong_AsInt(args[0]); + if (format_num == -1 && PyErr_Occurred()) { + return NULL; + } + + int silence_value = + SDL_GetSilenceValueForFormat((SDL_AudioFormat)format_num); + + return PyBytes_FromFormat("%c", silence_value); +} + +static PyMethodDef audio_methods[] = { + {"init", (PyCFunction)pg_audio_init, METH_NOARGS, NULL}, + {"quit", (PyCFunction)pg_audio_quit, METH_NOARGS, NULL}, + {"get_init", (PyCFunction)pg_audio_get_init, METH_NOARGS, NULL}, + {"get_current_driver", (PyCFunction)pg_audio_get_current_driver, + METH_NOARGS, NULL}, + {"get_drivers", (PyCFunction)pg_audio_get_drivers, METH_NOARGS, NULL}, + {"get_playback_device_states", + (PyCFunction)pg_audio_get_playback_device_states, METH_NOARGS, NULL}, + {"get_recording_device_states", + (PyCFunction)pg_audio_get_recording_device_states, METH_NOARGS, NULL}, + {"load_wav", (PyCFunction)pg_audio_load_wav, METH_O, NULL}, + {"get_default_playback_device_state", + (PyCFunction)pg_audio_get_default_playback_device_state, METH_NOARGS, + NULL}, + {"get_default_recording_device_state", + (PyCFunction)pg_audio_get_default_recording_device_state, METH_NOARGS, + NULL}, + + // format utility (the one) + {"get_silence_value_for_format", + (PyCFunction)pg_audio_get_silence_value_for_format, METH_FASTCALL, NULL}, + + // AudioDevice utilities + {"is_audio_device_playback", + (PyCFunction)pg_audio_is_audio_device_playback, METH_O, NULL}, + {"get_audio_device_name", (PyCFunction)pg_audio_get_audio_device_name, + METH_FASTCALL, NULL}, + {"get_audio_device_channel_map", + (PyCFunction)pg_audio_get_audio_device_channel_map, METH_O, NULL}, + {"open_audio_device", (PyCFunction)pg_audio_open_audio_device, + METH_FASTCALL, NULL}, + {"pause_audio_device", (PyCFunction)pg_audio_pause_audio_device, METH_O, + NULL}, + {"resume_audio_device", (PyCFunction)pg_audio_resume_audio_device, METH_O, + NULL}, + {"audio_device_paused", (PyCFunction)pg_audio_audio_device_paused, METH_O, + NULL}, + {"get_audio_device_gain", (PyCFunction)pg_audio_get_audio_device_gain, + METH_O, NULL}, + {"set_audio_device_gain", (PyCFunction)pg_audio_set_audio_device_gain, + METH_FASTCALL, NULL}, + + // AudioStream utilities + {"create_audio_stream", (PyCFunction)pg_audio_create_audio_stream, + METH_FASTCALL, NULL}, + {"bind_audio_stream", (PyCFunction)pg_audio_bind_audio_stream, + METH_FASTCALL, NULL}, + {"unbind_audio_stream", (PyCFunction)pg_audio_unbind_audio_stream, METH_O, + NULL}, + {"clear_audio_stream", (PyCFunction)pg_audio_clear_audio_stream, METH_O, + NULL}, + {"flush_audio_stream", (PyCFunction)pg_audio_flush_audio_stream, METH_O, + NULL}, + {"get_audio_stream_available", + (PyCFunction)pg_audio_get_audio_stream_available, METH_O, NULL}, + {"get_audio_stream_queued", (PyCFunction)pg_audio_get_audio_stream_queued, + METH_O, NULL}, + {"get_audio_stream_data", (PyCFunction)pg_audio_get_audio_stream_data, + METH_FASTCALL, NULL}, + {"put_audio_stream_data", (PyCFunction)pg_audio_put_audio_stream_data, + METH_FASTCALL, NULL}, + {"pause_audio_stream_device", + (PyCFunction)pg_audio_pause_audio_stream_device, METH_O, NULL}, + {"resume_audio_stream_device", + (PyCFunction)pg_audio_resume_audio_stream_device, METH_O, NULL}, + {"audio_stream_device_paused", + (PyCFunction)pg_audio_audio_stream_device_paused, METH_O, NULL}, + {"get_audio_stream_format", (PyCFunction)pg_audio_get_audio_stream_format, + METH_O, NULL}, + {"set_audio_stream_format", (PyCFunction)pg_audio_set_audio_stream_format, + METH_FASTCALL, NULL}, + {"get_audio_stream_gain", (PyCFunction)pg_audio_get_audio_stream_gain, + METH_O, NULL}, + {"set_audio_stream_gain", (PyCFunction)pg_audio_set_audio_stream_gain, + METH_FASTCALL, NULL}, + {"get_audio_stream_frequency_ratio", + (PyCFunction)pg_audio_get_audio_stream_frequency_ratio, METH_O, NULL}, + {"set_audio_stream_frequency_ratio", + (PyCFunction)pg_audio_set_audio_stream_frequency_ratio, METH_FASTCALL, + NULL}, + {"lock_audio_stream", (PyCFunction)pg_audio_lock_audio_stream, METH_O, + NULL}, + {"unlock_audio_stream", (PyCFunction)pg_audio_unlock_audio_stream, METH_O, + NULL}, + + {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; + } + + audio_state *state = GET_STATE(module); + state->audio_initialized = false; + + state->audio_device_state_type = + PyType_FromModuleAndSpec(module, &adevice_state_spec, NULL); + if (state->audio_device_state_type == NULL) { + return -1; + } + if (PyModule_AddType(module, + (PyTypeObject *)state->audio_device_state_type) < 0) { + return -1; + } + + state->audio_stream_state_type = + PyType_FromModuleAndSpec(module, &astream_state_spec, NULL); + if (state->audio_stream_state_type == NULL) { + return -1; + } + if (PyModule_AddType(module, + (PyTypeObject *)state->audio_stream_state_type) < 0) { + return -1; + } + + return 0; +} + +static int +pg_audio_traverse(PyObject *module, visitproc visit, void *arg) +{ + audio_state *state = GET_STATE(module); + Py_VISIT(state->audio_device_state_type); + Py_VISIT(state->audio_stream_state_type); + return 0; +} + +static int +pg_audio_clear(PyObject *module) +{ + audio_state *state = GET_STATE(module); + Py_CLEAR(state->audio_device_state_type); + Py_CLEAR(state->audio_stream_state_type); + return 0; +} + +static void +pg_audio_free(void *module) +{ + // Maybe not necessary, but lets tell SDL that we no longer depend + // on the audio subsystem when the module is being deallocated. + audio_state *state = GET_STATE((PyObject *)module); + if (state != NULL) { + if (state->audio_initialized) { + SDL_QuitSubSystem(SDL_INIT_AUDIO); + state->audio_initialized = false; + } + } + + // allow pg_audio_exec to omit calling pg_audio_clear on error + (void)pg_audio_clear((PyObject *)module); +} + +MODINIT_DEFINE(_base_audio) +{ + static PyModuleDef_Slot audio_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, "_base_audio", NULL, + sizeof(audio_state), audio_methods, audio_slots, + pg_audio_traverse, pg_audio_clear, pg_audio_free}; + + return PyModuleDef_Init(&_module); +} diff --git a/src_c/meson.build b/src_c/meson.build index d8591fd970..a328133b10 100644 --- a/src_c/meson.build +++ b/src_c/meson.build @@ -420,10 +420,21 @@ if sdl_ttf_dep.found() ) endif -# TODO: support SDL3 -if sdl_api != 3 +# C-backing for new in SDL3, audio module. +if sdl_api == 3 + base_audio = py.extension_module( + '_base_audio', + '_base_audio.c', + c_args: warnings_error, + dependencies: pg_base_deps, + install: true, + subdir: pg, + ) +endif +# TODO: support SDL3 if sdl_mixer_dep.found() +if sdl_api != 3 mixer = py.extension_module( 'mixer', 'mixer.c', diff --git a/src_py/_audio.py b/src_py/_audio.py new file mode 100644 index 0000000000..7edf956f58 --- /dev/null +++ b/src_py/_audio.py @@ -0,0 +1,444 @@ +import weakref + +import pygame.base +from pygame import _base_audio # pylint: disable=no-name-in-module +from pygame.typing import FileLike + +# TODO: Docs +# TODO: Tests +# TODO: make it safe to quit the audio subsystem (e.g. what happens with +# the objects that are now invalid in SDL's eyes.) + + +class AudioFormat: + # AudioFormat details pulled from SDL_audio.h header files + # These details are stable for the lifetime of SDL3, as programs built + # on one release will be able to run on newer releases. + _MASK_BITSIZE = 0xFF + _MASK_FLOAT = 1 << 8 + _MASK_BIG_ENDIAN = 1 << 12 + _MASK_SIGNED = 1 << 15 + + # These objects are constructed externally, putting these here + # to annotate the attributes that are populated. + _value: int + _name: str + + @property + def bitsize(self) -> int: + return self._value & AudioFormat._MASK_BITSIZE + + @property + def bytesize(self) -> int: + return self.bitsize // 8 + + @property + def is_float(self) -> bool: + return bool(self._value & AudioFormat._MASK_FLOAT) + + @property + def is_int(self) -> bool: + return not self.is_float + + @property + def is_big_endian(self) -> bool: + return bool(self._value & AudioFormat._MASK_BIG_ENDIAN) + + @property + def is_little_endian(self) -> bool: + return not self.is_big_endian + + @property + def is_signed(self) -> bool: + return bool(self._value & AudioFormat._MASK_SIGNED) + + @property + def is_unsigned(self) -> bool: + return not self.is_signed + + @property + def name(self) -> str: + return self._name + + @property + def silence_value(self) -> bytes: + return _base_audio.get_silence_value_for_format(self._value) + + # TODO maybe unnecessary? + def __index__(self) -> int: + """Returns the actual constant value needed for calls to SDL""" + return self._value + + def __repr__(self) -> str: + return f"pygame.audio.{self._name}" + + +class AudioSpec: + def __init__(self, format: AudioFormat, channels: int, frequency: int) -> None: + if not isinstance(format, AudioFormat): + raise TypeError( + f"AudioSpec format must be an AudioFormat, received {type(format)}" + ) + + if channels < 1 or channels > 8: + raise ValueError("Invalid channel count, should be between 1 and 8.") + + # AudioSpecs are immutable so that they can be owned by other things + # like AudioStreams without worrying about what happens if someone + # changes the spec externally. + self._format = format + self._channels = channels + self._frequency = frequency + + @property + def format(self) -> AudioFormat: + return self._format + + @property + def channels(self) -> int: + return self._channels + + @property + def frequency(self) -> int: + return self._frequency + + @property + def framesize(self) -> int: + return self._format.bytesize * self.channels + + def __repr__(self) -> str: + return ( + self.__class__.__name__ + + f"({self._format}, {self._channels}, {self._frequency})" + ) + + +class AudioDevice: + # def __repr__(self) -> str: + # return f"AudioDevice with name {self.name}" + + def open(self, spec: AudioSpec | None = None) -> "LogicalAudioDevice": + if spec is None: + dev_state = _base_audio.open_audio_device(self._state) + elif isinstance(spec, AudioSpec): + dev_state = _base_audio.open_audio_device( + self._state, spec.format, spec.channels, spec.frequency + ) + else: + raise TypeError( + "AudioDevice open 'spec' argument must be an AudioSpec " + f"or None, received {type(spec)}" + ) + + device = object.__new__(LogicalAudioDevice) + device._state = dev_state + return device + + @property + def is_playback(self) -> bool: + return _base_audio.is_audio_device_playback(self._state) + + # TODO: this doesn't work for the default device ids... + @property + def name(self) -> str: + return _base_audio.get_audio_device_name(self._state) + + @property + def channel_map(self) -> list[int] | None: + return _base_audio.get_audio_device_channel_map(self._state) + + +class LogicalAudioDevice(AudioDevice): + def pause(self) -> None: + _base_audio.pause_audio_device(self._state) + + def resume(self) -> None: + _base_audio.resume_audio_device(self._state) + + @property + def paused(self) -> bool: + return _base_audio.audio_device_paused(self._state) + + @property + def gain(self) -> float: + return _base_audio.get_audio_device_gain(self._state) + + @gain.setter + def gain(self, value: float) -> None: + _base_audio.set_audio_device_gain(self._state, value) + + +class AudioStream: + def __init__(self, src_spec: AudioSpec, dst_spec: AudioSpec) -> None: + if not isinstance(src_spec, AudioSpec): + raise TypeError( + f"AudioStream src_spec must be an AudioSpec, received {type(src_spec)}" + ) + if not isinstance(dst_spec, AudioSpec): + raise TypeError( + f"AudioStream dst_spec must be an AudioSpec, received {type(dst_spec)}" + ) + + self._state = _base_audio.create_audio_stream( + src_spec.format, + src_spec.channels, + src_spec.frequency, + dst_spec.format, + dst_spec.channels, + dst_spec.frequency, + ) + self._device: LogicalAudioDevice | None = None + + def bind(self, device: LogicalAudioDevice) -> None: + if not isinstance(device, LogicalAudioDevice): + raise TypeError( + f"AudioStream bind argument must be LogicalAudioDevice, received {type(device)}" + ) + + _base_audio.bind_audio_stream(device._state, self._state) + self._device = device + + def unbind(self) -> None: + _base_audio.unbind_audio_stream(self._state) + self._device = None + + def clear(self) -> None: + _base_audio.clear_audio_stream(self._state) + + def flush(self) -> None: + _base_audio.flush_audio_stream(self._state) + + @property + def num_available_bytes(self) -> int: + return _base_audio.get_audio_stream_available(self._state) + + @property + def num_queued_bytes(self) -> int: + return _base_audio.get_audio_stream_queued(self._state) + + def get_data(self, size: int) -> bytes: + return _base_audio.get_audio_stream_data(self._state, size) + + # TODO: replace bytes | bytearray | memoryview with collections.abc.Buffer + # when we support only 3.12 and up. + def put_data(self, data: bytes | bytearray | memoryview) -> None: + _base_audio.put_audio_stream_data(self._state, data) + + def pause_device(self) -> None: + _base_audio.pause_audio_stream_device(self._state) + + def resume_device(self) -> None: + _base_audio.resume_audio_stream_device(self._state) + + @property + def device_paused(self) -> bool: + return _base_audio.audio_stream_device_paused(self._state) + + @property + def device(self) -> LogicalAudioDevice | None: + return self._device + + @property + def src_spec(self) -> AudioSpec: + return _internals.audio_spec_from_ints( + *_base_audio.get_audio_stream_format(self._state)[0:3] + ) + + @src_spec.setter + def src_spec(self, value: AudioSpec) -> None: + if not isinstance(value, AudioSpec): + raise TypeError( + f"AudioStream src_spec must be an AudioSpec, received {type(value)}" + ) + + # If bound to a non-playback device (e.g. recording device), the input + # spec can't be changed. SDL itself will ignore these changes, + # but we are erroring to let the users know not to do this. + if self.device is not None and not self.device.is_playback: + raise pygame.error( + "Cannot change stream src spec while bound to a recording device" + ) + + _base_audio.set_audio_stream_format( + self._state, (value.format, value.channels, value.frequency), None + ) + + @property + def dst_spec(self) -> AudioSpec: + # My first impulse here was to store the Python dst_spec AudioSpec + # object and just return it here. BUT, SDL can change the output + # format of the stream internally- + # Only when it gets bound? + # To guarantee correctness it now pulls it every time, even though + # that is inefficient. + + return _internals.audio_spec_from_ints( + *_base_audio.get_audio_stream_format(self._state)[3:6] + ) + + @dst_spec.setter + def dst_spec(self, value: AudioSpec) -> None: + if not isinstance(value, AudioSpec): + raise TypeError( + f"AudioStream dst_spec must be an AudioSpec, received {type(value)}" + ) + + # If bound to a playback device, the output spec can't be changed. + # SDL itself will ignore these changes, but we are erroring to let the users + # know not to do this. + if self.device is not None and self.device.is_playback: + raise pygame.error( + "Cannot change stream dst spec while bound to a playback device" + ) + + _base_audio.set_audio_stream_format( + self._state, None, (value.format, value.channels, value.frequency) + ) + + @property + def gain(self) -> float: + return _base_audio.get_audio_stream_gain(self._state) + + @gain.setter + def gain(self, value: float) -> None: + if value < 0: + raise ValueError("Gain must be >= 0.") + + _base_audio.set_audio_stream_gain(self._state, value) + + @property + def frequency_ratio(self) -> float: + return _base_audio.get_audio_stream_frequency_ratio(self._state) + + @frequency_ratio.setter + def frequency_ratio(self, value: float) -> None: + if value <= 0 or value > 10: + raise ValueError( + "Frequency ratio must be between 0 and 10. (0 < ratio <= 10)" + ) + + _base_audio.set_audio_stream_frequency_ratio(self._state, value) + + def lock(self) -> None: + _base_audio.lock_audio_stream(self._state) + + def unlock(self) -> None: + _base_audio.unlock_audio_stream(self._state) + + def __repr__(self) -> str: + audio_format_ints = _base_audio.get_audio_stream_format(self._state) + src_spec = _internals.audio_spec_from_ints(*audio_format_ints[0:3]) + dst_spec = _internals.audio_spec_from_ints(*audio_format_ints[3:6]) + + return f"<{self.__class__.__name__}({src_spec}, {dst_spec})>" + + +class AudioInternals: + def __init__(self) -> None: + self._int_to_format: dict[int, AudioFormat] = {} + self._id_to_device = weakref.WeakValueDictionary() + + def create_format(self, name: str, value: int) -> AudioFormat: + audio_format = object.__new__(AudioFormat) + audio_format._name = name + audio_format._value = value + self._int_to_format[value] = audio_format + return audio_format + + def audio_spec_from_ints( + self, format_num: int, channels: int, frequency: int + ) -> AudioSpec: + format_inst = self._int_to_format.get(format_num) + if format_inst is None: + raise pygame.error( + f"Unknown audio format value {format_num}. " + "Please report to the pygame-ce devs." + ) + return AudioSpec(format_inst, channels, frequency) + + def create_audio_device(self, dev_state) -> AudioDevice: + # If a Python AudioDevice is already allocated with that id, use it. + # Otherwise allocate a new AudioDevice and set its state. + device = self._id_to_device.get(dev_state.id) + if device is None: + device = object.__new__(AudioDevice) + device._state = dev_state + self._id_to_device[dev_state.id] = device + return device + + +_internals = AudioInternals() + + +UNKNOWN = _internals.create_format("UNKNOWN", 0x0000) +U8 = _internals.create_format("U8", 0x0008) +S8 = _internals.create_format("S8", 0x8008) +S16LE = _internals.create_format("S16LE", 0x8010) +S16BE = _internals.create_format("S16BE", 0x9010) +S32LE = _internals.create_format("S32LE", 0x8020) +S32BE = _internals.create_format("S32BE", 0x9020) +F32LE = _internals.create_format("F32LE", 0x8120) +F32BE = _internals.create_format("F32BE", 0x9120) + +if pygame.base.get_sdl_byteorder() == 1234: + S16 = S16LE + S32 = S32LE + F32 = F32LE +else: + S16 = S16BE + S32 = S32BE + F32 = F32BE + + +def init() -> None: + _base_audio.init() + + +# See TODO above, this is currently not safe to offer +# def quit() -> None: +# _base_audio.quit() + + +def get_init() -> bool: + return _base_audio.get_init() + + +def get_current_driver() -> str: + return _base_audio.get_current_driver() + + +def get_drivers() -> list[str]: + return _base_audio.get_drivers() + + +def get_playback_devices() -> list[AudioDevice]: + return [ + _internals.create_audio_device(dev_state) + for dev_state in _base_audio.get_playback_device_states() + ] + + +def get_recording_devices() -> list[AudioDevice]: + return [ + _internals.create_audio_device(dev_state) + for dev_state in _base_audio.get_recording_device_states() + ] + + +def load_wav(file: FileLike) -> tuple[AudioSpec, bytes]: + audio_bytes, format_num, channels, frequency = _base_audio.load_wav(file) + return ( + _internals.audio_spec_from_ints(format_num, channels, frequency), + audio_bytes, + ) + + +DEFAULT_PLAYBACK_DEVICE = _internals.create_audio_device( + _base_audio.get_default_playback_device_state() +) +DEFAULT_RECORDING_DEVICE = _internals.create_audio_device( + _base_audio.get_default_recording_device_state() +) + +# Don't re-export names if it can be helped +del weakref, FileLike, AudioInternals diff --git a/src_py/meson.build b/src_py/meson.build index 541c54cd69..b85d194f7c 100644 --- a/src_py/meson.build +++ b/src_py/meson.build @@ -28,6 +28,10 @@ if not sdl_ttf_dep.found() and freetype_dep.found() py.install_sources('ftfont.py', subdir: pg, rename: 'font.py') endif +if sdl_api == 3 + py.install_sources('_audio.py', subdir: pg) +endif + data_files = files( 'freesansbold.ttf', 'pygame_icon.bmp', From 78396bb247d1fc09f67333c20dced6cb1b90908f Mon Sep 17 00:00:00 2001 From: Starbuck5 <46412508+Starbuck5@users.noreply.github.com> Date: Sat, 20 Dec 2025 21:17:15 -0800 Subject: [PATCH 02/10] _audio: use METH_O when possible --- src_c/_base_audio.c | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src_c/_base_audio.c b/src_c/_base_audio.c index 3ba2b45496..af7c273460 100644 --- a/src_c/_base_audio.c +++ b/src_c/_base_audio.c @@ -91,12 +91,12 @@ pg_audio_is_audio_device_playback(PyObject *module, PyObject *arg) } static PyObject * -pg_audio_get_audio_device_name(PyObject *module, PyObject *const *args, - Py_ssize_t nargs) +pg_audio_get_audio_device_name(PyObject *module, PyObject *arg) { - // assert nargs == 1 - // assert type(args[0]) == AudioDeviceState - SDL_AudioDeviceID devid = ((PGAudioDeviceStateObject *)args[0])->devid; + // SDL_GetAudioDeviceName + // arg: PGAudioDeviceStateObject + + SDL_AudioDeviceID devid = ((PGAudioDeviceStateObject *)arg)->devid; const char *name = SDL_GetAudioDeviceName(devid); if (name == NULL) { return RAISE(pgExc_SDLError, SDL_GetError()); @@ -932,13 +932,12 @@ pg_audio_get_default_recording_device_state(PyObject *module, PyObject *_null) } static PyObject * -pg_audio_get_silence_value_for_format(PyObject *module, PyObject *const *args, - Py_ssize_t nargs) +pg_audio_get_silence_value_for_format(PyObject *module, PyObject *arg) { // SDL_GetSilenceValueForFormat // format: int - int format_num = PyLong_AsInt(args[0]); + int format_num = PyLong_AsInt(arg); if (format_num == -1 && PyErr_Occurred()) { return NULL; } @@ -970,13 +969,13 @@ static PyMethodDef audio_methods[] = { // format utility (the one) {"get_silence_value_for_format", - (PyCFunction)pg_audio_get_silence_value_for_format, METH_FASTCALL, NULL}, + (PyCFunction)pg_audio_get_silence_value_for_format, METH_O, NULL}, // AudioDevice utilities {"is_audio_device_playback", (PyCFunction)pg_audio_is_audio_device_playback, METH_O, NULL}, {"get_audio_device_name", (PyCFunction)pg_audio_get_audio_device_name, - METH_FASTCALL, NULL}, + METH_O, NULL}, {"get_audio_device_channel_map", (PyCFunction)pg_audio_get_audio_device_channel_map, METH_O, NULL}, {"open_audio_device", (PyCFunction)pg_audio_open_audio_device, From 561a5ae14eaba2f517303ff6e8c1e5254c7125f1 Mon Sep 17 00:00:00 2001 From: Starbuck5 <46412508+Starbuck5@users.noreply.github.com> Date: Sat, 20 Dec 2025 22:10:01 -0800 Subject: [PATCH 03/10] Add bounds checking to device gain --- src_py/_audio.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src_py/_audio.py b/src_py/_audio.py index 7edf956f58..a0a69c9517 100644 --- a/src_py/_audio.py +++ b/src_py/_audio.py @@ -139,6 +139,7 @@ def is_playback(self) -> bool: return _base_audio.is_audio_device_playback(self._state) # TODO: this doesn't work for the default device ids... + # https://github.com/libsdl-org/SDL/issues/14615 @property def name(self) -> str: return _base_audio.get_audio_device_name(self._state) @@ -165,6 +166,8 @@ def gain(self) -> float: @gain.setter def gain(self, value: float) -> None: + if value < 0: + raise ValueError("Gain must be >= 0.") _base_audio.set_audio_device_gain(self._state, value) From 262f3f94f0d07fee1a3ddeec543848ae1cf3aacd Mon Sep 17 00:00:00 2001 From: Starbuck5 <46412508+Starbuck5@users.noreply.github.com> Date: Fri, 30 Jan 2026 00:10:48 -0800 Subject: [PATCH 04/10] For some reason I need structmember now --- src_c/_base_audio.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src_c/_base_audio.c b/src_c/_base_audio.c index af7c273460..36ef48d228 100644 --- a/src_c/_base_audio.c +++ b/src_c/_base_audio.c @@ -1,5 +1,6 @@ #include "pygame.h" #include "pgcompat.h" +#include "structmember.h" // Useful heap type example @ // https://github.com/python/cpython/blob/main/Modules/xxlimited.c From c5ca2b03f6089665447ab9f1a876e8bf5dd3715f Mon Sep 17 00:00:00 2001 From: Starbuck5 <46412508+Starbuck5@users.noreply.github.com> Date: Sat, 31 Jan 2026 00:28:38 -0800 Subject: [PATCH 05/10] Fix potential overflow, adjust func --- src_c/_base_audio.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src_c/_base_audio.c b/src_c/_base_audio.c index 36ef48d228..39488b8eea 100644 --- a/src_c/_base_audio.c +++ b/src_c/_base_audio.c @@ -456,6 +456,10 @@ pg_audio_get_audio_stream_data(PyObject *module, PyObject *const *args, return NULL; } + if (size < 0) { + return RAISE(PyExc_ValueError, "size must be >= 0"); + } + void *buf = malloc(size); if (buf == NULL) { return PyErr_NoMemory(); @@ -1041,7 +1045,7 @@ static PyMethodDef audio_methods[] = { // MODULE SETUP // *************************************************************************** -int +static int pg_audio_exec(PyObject *module) { /*imported needed apis*/ From b340b43d3d4f2f2c51af9dda453c0c27b509ae7f Mon Sep 17 00:00:00 2001 From: Starbuck5 <46412508+Starbuck5@users.noreply.github.com> Date: Sat, 31 Jan 2026 00:50:28 -0800 Subject: [PATCH 06/10] Fix 0 byte case and remove unnecessary error block --- src_c/_base_audio.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src_c/_base_audio.c b/src_c/_base_audio.c index 39488b8eea..3c74b49411 100644 --- a/src_c/_base_audio.c +++ b/src_c/_base_audio.c @@ -460,6 +460,10 @@ pg_audio_get_audio_stream_data(PyObject *module, PyObject *const *args, return RAISE(PyExc_ValueError, "size must be >= 0"); } + if (size == 0) { + return PyBytes_FromStringAndSize("", 0); + } + void *buf = malloc(size); if (buf == NULL) { return PyErr_NoMemory(); @@ -474,10 +478,6 @@ pg_audio_get_audio_stream_data(PyObject *module, PyObject *const *args, PyObject *bytes = PyBytes_FromStringAndSize(buf, bytes_read); free(buf); - if (bytes == NULL) { - return NULL; - } - return bytes; } From 371f337edecdf7791b26012705a0785a3a151c31 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 07/10] 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. --- 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 | 1464 ++++++++++++++++++++++ src_c/meson.build | 10 +- src_py/_sdl3_mixer.py | 105 ++ src_py/meson.build | 1 + 11 files changed, 1784 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/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..5409699481 --- /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 + +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): ... + # 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): ... + # 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): ... + @property + def frequency_ratio(self) -> float: ... + @frequency_ratio.setter + def frequency_ratio(self, value: float): ... + # 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 5ddffecc89..a44cdfceec 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 46b375aa83..3cbe33f20a 100644 --- a/meson.build +++ b/meson.build @@ -149,7 +149,7 @@ if plat == 'win' and host_machine.cpu_family().startswith('x86') # consider meson wraps? Hopefully can also get the same build path as below sdl_ver = (sdl_api == 3) ? '3.4.0' : '2.32.10' sdl_image_ver = (sdl_api == 3) ? '3.4.0' : '2.8.8' - sdl_mixer_ver = '2.8.1' + sdl_mixer_ver = (sdl_api == 3) ? '3.1.2' : '2.8.1' 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 3c74b49411..dd2860ec71 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..3fc433cac0 --- /dev/null +++ b/src_c/_sdl3_mixer_c.c @@ -0,0 +1,1464 @@ +#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; +} _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)) { + return NULL; + } + + 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|LLLLLLLLLLL", 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)) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + 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, "|L", 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, "s", 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 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; +} + +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)) { + 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 + PyErr_SetString(PyExc_TypeError, "argument 3 must be Mixer or None"); + return -1; + } + + 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; +} + +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 +} + +// NOT FINISHED, NEEDS AUDIOSPEC SUPPORT FIRST. +static PyObject * +pg_audio_obj_from_raw(PyTypeObject *cls, PyObject *args, PyObject *kwargs) +{ + PyObject *buffer; + PyObject *mixer_or_none = Py_None; + char *keywords[] = {"buffer", "preferred_mixer", NULL}; + PyObject *mixer_type = + PyObject_GetAttrString((PyObject *)cls, "_mixer_type"); + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|O", keywords, &buffer, + &mixer_or_none)) { + 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 + return RAISE(PyExc_TypeError, "argument 2 must be Mixer or None"); + } + + PyObject *bytes = PyBytes_FromObject(buffer); + if (bytes == NULL) { + return NULL; + } + + PGAudioObject *self = (PGAudioObject *)cls->tp_alloc(cls, 0); + if (self == NULL) { + Py_DECREF(bytes); + return NULL; + } + Py_INCREF(self); + + // MIX_LoadRawAudio(mixer, ); + // Py_DECREF(bytes); + // printf("buffer=%p, bytes=%p\n", buffer, bytes); + + Py_RETURN_NONE; +} + +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)) { + 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 + return RAISE(PyExc_TypeError, "argument 3 must be Mixer or None"); + } + + PGAudioObject *self = (PGAudioObject *)cls->tp_alloc(cls, 0); + if (self == NULL) { + return NULL; + } + Py_INCREF(self); + + // 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) { + 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); + } + + 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; +} + +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; + Py_XDECREF(self->mixer_obj); + self->mixer_obj = NULL; +} + +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 (!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)) { + 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 + return RAISE(PyExc_TypeError, "argument 1 must be Audio or None"); + } + + 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; +} + +#define SET_NUM_PROPERTY_IFNOTDEFAULT_ANDFLAG(props, property, value, \ + default, success) \ + if (value != default) { \ + success &= SDL_SetNumberProperty(props, property, value); \ + } + +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)) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + 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 +exec_mixer(PyObject *module) +{ + /*imported needed apis*/ + import_pygame_base(); + if (PyErr_Occurred()) { + return -1; + } + import_pygame_rwobject(); + if (PyErr_Occurred()) { + return -1; + } + + PyObject *mixer_type = PyType_FromModuleAndSpec(module, &mixer_spec, NULL); + if (PyModule_AddObjectRef(module, "Mixer", mixer_type) < 0) { + return -1; + } + + PyObject *audio_type = PyType_FromModuleAndSpec(module, &audio_spec, NULL); + if (PyModule_AddObjectRef(module, "Audio", audio_type) < 0) { + return -1; + } + + PyObject *track_type = PyType_FromModuleAndSpec(module, &track_spec, NULL); + if (PyModule_AddObjectRef(module, "Track", track_type) < 0) { + return -1; + } + + if (PyObject_SetAttrString(mixer_type, "_audio_type", audio_type) < 0) { + return -1; + } + if (PyObject_SetAttrString(track_type, "_audio_type", audio_type) < 0) { + return -1; + } + if (PyObject_SetAttrString(audio_type, "_mixer_type", mixer_type) < 0) { + return -1; + } + + _mixer_state *state = GET_STATE(module); + state->mixer_initialized = false; + + return 0; +} + +MODINIT_DEFINE(_sdl3_mixer_c) +{ + static PyModuleDef_Slot mixer_slots[] = { + {Py_mod_exec, &exec_mixer}, +#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, + "_sdl3_mixer_c", + "DOC TODO", + sizeof(_mixer_state), + _mixer_methods, + mixer_slots, + NULL, + NULL, + NULL}; + + return PyModuleDef_Init(&_module); +} diff --git a/src_c/meson.build b/src_c/meson.build index a328133b10..ca7d644dcd 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..545482aa91 --- /dev/null +++ b/src_py/_sdl3_mixer.py @@ -0,0 +1,105 @@ +import dataclasses + +from pygame import _audio as audio, _sdl3_mixer_c + +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 positive.") + + return 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 positive.") + + return 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): + @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 676978c35527575fb9c896c45a12db4efb9d35fe Mon Sep 17 00:00:00 2001 From: Starbuck5 <46412508+Starbuck5@users.noreply.github.com> Date: Sun, 1 Feb 2026 02:56:38 -0800 Subject: [PATCH 08/10] Address mistakes and job fails --- .github/workflows/build-sdl3.yml | 11 +++++++ buildconfig/stubs/pygame/_sdl3_mixer.pyi | 2 +- src_c/_sdl3_mixer_c.c | 38 ++++++++++++++++++++---- src_py/_sdl3_mixer.py | 4 +-- 4 files changed, 47 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build-sdl3.yml b/.github/workflows/build-sdl3.yml index 12b87e2a2c..cd9433194b 100644 --- a/.github/workflows/build-sdl3.yml +++ b/.github/workflows/build-sdl3.yml @@ -113,6 +113,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/pygame/_sdl3_mixer.pyi b/buildconfig/stubs/pygame/_sdl3_mixer.pyi index 5409699481..eb904edad5 100644 --- a/buildconfig/stubs/pygame/_sdl3_mixer.pyi +++ b/buildconfig/stubs/pygame/_sdl3_mixer.pyi @@ -2,7 +2,7 @@ import dataclasses from collections.abc import Callable from typing import Any, Type, TypedDict, TypeVar -import _audio as audio +from pygame import _audio as audio from pygame.typing import FileLike from typing_extensions import Buffer diff --git a/src_c/_sdl3_mixer_c.c b/src_c/_sdl3_mixer_c.c index 3fc433cac0..3b25451b74 100644 --- a/src_c/_sdl3_mixer_c.c +++ b/src_c/_sdl3_mixer_c.c @@ -97,8 +97,10 @@ pg_mixer_obj_play_audio(PGMixerObject *self, PyObject *args, PyObject *kwargs) 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()); @@ -124,7 +126,7 @@ pg_mixer_obj_play_tag(PGMixerObject *self, PyObject *args, PyObject *kwargs) NULL}; if (!PyArg_ParseTupleAndKeywords( - args, kwargs, "s|LLLLLLLLLLL", keywords, &tag, &loops, &max_ms, + args, kwargs, "s|LLLLLL", keywords, &tag, &loops, &max_ms, &start_ms, &loop_start_ms, &fadein_ms, &append_silence_ms)) { return NULL; } @@ -209,7 +211,7 @@ pg_mixer_obj_set_tag_gain(PGMixerObject *self, PyObject *args, float gain; char *keywords[] = {"tag", "gain", NULL}; - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|L", keywords, &tag, + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "sf", keywords, &tag, &gain)) { return NULL; } @@ -228,7 +230,7 @@ pg_mixer_obj_stop_all_tracks(PGMixerObject *self, PyObject *args, int64_t fade_out_ms = 0; char *keywords[] = {"fade_out_ms", NULL}; - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s", keywords, + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|L", keywords, &fade_out_ms)) { return NULL; } @@ -316,6 +318,11 @@ 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 * @@ -405,6 +412,7 @@ pg_audio_obj_init(PGAudioObject *self, PyObject *args, PyObject *kwargs) if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|pO", keywords, &file, &predecode, &mixer_or_none)) { + Py_DECREF(mixer_type); return -1; } @@ -413,9 +421,11 @@ pg_audio_obj_init(PGAudioObject *self, PyObject *args, PyObject *kwargs) 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) { @@ -436,6 +446,11 @@ 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 * @@ -481,6 +496,7 @@ pg_audio_obj_from_raw(PyTypeObject *cls, PyObject *args, PyObject *kwargs) if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|O", keywords, &buffer, &mixer_or_none)) { + Py_DECREF(mixer_type); return NULL; } @@ -489,8 +505,10 @@ pg_audio_obj_from_raw(PyTypeObject *cls, PyObject *args, PyObject *kwargs) 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 2 must be Mixer or None"); } + Py_DECREF(mixer_type); PyObject *bytes = PyBytes_FromObject(buffer); if (bytes == NULL) { @@ -524,6 +542,7 @@ pg_audio_obj_from_sine_wave(PyTypeObject *cls, PyObject *args, if (!PyArg_ParseTupleAndKeywords(args, kwargs, "if|Oi", keywords, &hz, &litude, &mixer_or_none, &ms)) { + Py_DECREF(mixer_type); return NULL; } @@ -532,8 +551,10 @@ pg_audio_obj_from_sine_wave(PyTypeObject *cls, PyObject *args, 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) { @@ -736,8 +757,12 @@ pg_track_obj_dealloc(PGTrackObject *self) { MIX_DestroyTrack(self->track); self->track = NULL; - Py_XDECREF(self->mixer_obj); - self->mixer_obj = NULL; + Py_CLEAR(self->mixer_obj); + PyObject_GC_UnTrack(self); + PyTypeObject *tp = Py_TYPE(self); + freefunc free = PyType_GetSlot(tp, Py_tp_free); + free(self); + Py_DECREF(tp); } static PyObject * @@ -816,6 +841,7 @@ pg_track_obj_set_audio(PGTrackObject *self, PyObject *args, PyObject *kwargs) if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O", keywords, &audio_or_none)) { + Py_DECREF(audio_type); return NULL; } @@ -824,8 +850,10 @@ pg_track_obj_set_audio(PGTrackObject *self, PyObject *args, PyObject *kwargs) 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()); diff --git a/src_py/_sdl3_mixer.py b/src_py/_sdl3_mixer.py index 545482aa91..b578c74edf 100644 --- a/src_py/_sdl3_mixer.py +++ b/src_py/_sdl3_mixer.py @@ -14,7 +14,7 @@ def ms_to_frames(sample_rate: int, ms: int) -> int: if ms < 0: raise ValueError("MS must be positive.") - return ms / 1000 * sample_rate + return int(ms / 1000 * sample_rate) # Pure Python version of MIX_FramesToMS, since it's so straightforward. @@ -24,7 +24,7 @@ def frames_to_ms(sample_rate: int, frames: int) -> int: if frames < 0: raise ValueError("Frames must be positive.") - return frames / sample_rate * 1000 + return int(frames / sample_rate * 1000) get_decoders = _sdl3_mixer_c.get_decoders From a4b950545630e0a323cd48b6adafa17af547a989 Mon Sep 17 00:00:00 2001 From: Starbuck5 <46412508+Starbuck5@users.noreply.github.com> Date: Sun, 1 Feb 2026 03:31:50 -0800 Subject: [PATCH 09/10] Implement Audio.from_raw, hopefully fix some things --- buildconfig/stubs/pygame/_sdl3_mixer.pyi | 8 ++-- src_c/_sdl3_mixer_c.c | 51 +++++++++++++++++------- src_py/_sdl3_mixer.py | 15 ++++++- 3 files changed, 55 insertions(+), 19 deletions(-) diff --git a/buildconfig/stubs/pygame/_sdl3_mixer.pyi b/buildconfig/stubs/pygame/_sdl3_mixer.pyi index eb904edad5..870d1548e8 100644 --- a/buildconfig/stubs/pygame/_sdl3_mixer.pyi +++ b/buildconfig/stubs/pygame/_sdl3_mixer.pyi @@ -71,10 +71,10 @@ class Audio: 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_raw( + cls, buffer: Buffer, spec: audio.AudioSpec, preferred_mixer: Mixer | None = None + ) -> Audio: ... @classmethod def from_sine_wave( cls, diff --git a/src_c/_sdl3_mixer_c.c b/src_c/_sdl3_mixer_c.c index 3b25451b74..32006d6e0d 100644 --- a/src_c/_sdl3_mixer_c.c +++ b/src_c/_sdl3_mixer_c.c @@ -484,29 +484,43 @@ pg_audio_obj_get_duration_infinite(PGAudioObject *self, void *_null) Py_RETURN_FALSE; // not infinite / unknown } -// NOT FINISHED, NEEDS AUDIOSPEC SUPPORT FIRST. static PyObject * pg_audio_obj_from_raw(PyTypeObject *cls, PyObject *args, PyObject *kwargs) { - PyObject *buffer; + PyObject *buffer, *spec_obj; PyObject *mixer_or_none = Py_None; - char *keywords[] = {"buffer", "preferred_mixer", NULL}; + char *keywords[] = {"buffer", "spec", "preferred_mixer", NULL}; PyObject *mixer_type = PyObject_GetAttrString((PyObject *)cls, "_mixer_type"); - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|O", keywords, &buffer, - &mixer_or_none)) { + 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 2 must be Mixer or None"); + return RAISE(PyExc_TypeError, "argument 3 must be Mixer or None"); } Py_DECREF(mixer_type); @@ -515,18 +529,27 @@ pg_audio_obj_from_raw(PyTypeObject *cls, PyObject *args, PyObject *kwargs) 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; } - Py_INCREF(self); - // MIX_LoadRawAudio(mixer, ); - // Py_DECREF(bytes); - // printf("buffer=%p, bytes=%p\n", buffer, bytes); + MIX_Audio *raw_audio = MIX_LoadRawAudio(mixer, buf, (size_t)len, &spec); + if (raw_audio == NULL) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + self->audio = raw_audio; - Py_RETURN_NONE; + Py_INCREF(self); + return (PyObject *)self; } static PyObject * @@ -560,7 +583,6 @@ pg_audio_obj_from_sine_wave(PyTypeObject *cls, PyObject *args, if (self == NULL) { return NULL; } - Py_INCREF(self); // MIX_CreateSineWaveAudio is bugged right now (2025-10-04), // complains about invalid context parameter. @@ -571,6 +593,7 @@ pg_audio_obj_from_sine_wave(PyTypeObject *cls, PyObject *args, } self->audio = sine_wave_audio; + Py_INCREF(self); return (PyObject *)self; } @@ -697,8 +720,8 @@ static PyGetSetDef audio_obj_getsets[] = { 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"}, + {"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, diff --git a/src_py/_sdl3_mixer.py b/src_py/_sdl3_mixer.py index b578c74edf..7a3ba0befa 100644 --- a/src_py/_sdl3_mixer.py +++ b/src_py/_sdl3_mixer.py @@ -1,6 +1,6 @@ import dataclasses -from pygame import _audio as audio, _sdl3_mixer_c +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 @@ -66,6 +66,19 @@ class AudioMetadata: 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"Track 'spec' argument must be an AudioSpec, received {type(spec)}" + ) + + return _sdl3_mixer_c.Audio.from_raw( + buffer, (spec.format, spec.channels, spec.frequency), preferred_mixer + ) + @property def spec(self) -> audio.AudioSpec: return audio._internals.audio_spec_from_ints( From 2c775096a6dcabf65b6895a73e6926acfef9273d Mon Sep 17 00:00:00 2001 From: Starbuck5 <46412508+Starbuck5@users.noreply.github.com> Date: Sun, 1 Feb 2026 13:17:40 -0800 Subject: [PATCH 10/10] Fix a few ref handling errors, other errors --- src_c/_sdl3_mixer_c.c | 22 +++++++++++++--------- src_py/_sdl3_mixer.py | 2 +- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src_c/_sdl3_mixer_c.c b/src_c/_sdl3_mixer_c.c index 32006d6e0d..ec6de7ca2e 100644 --- a/src_c/_sdl3_mixer_c.c +++ b/src_c/_sdl3_mixer_c.c @@ -144,9 +144,11 @@ pg_mixer_obj_play_tag(PGMixerObject *self, PyObject *args, PyObject *kwargs) 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; } @@ -544,11 +546,13 @@ pg_audio_obj_from_raw(PyTypeObject *cls, PyObject *args, PyObject *kwargs) 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_INCREF(self); + Py_DECREF(bytes); return (PyObject *)self; } @@ -589,11 +593,11 @@ pg_audio_obj_from_sine_wave(PyTypeObject *cls, PyObject *args, 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; - Py_INCREF(self); return (PyObject *)self; } @@ -780,8 +784,9 @@ pg_track_obj_dealloc(PGTrackObject *self) { MIX_DestroyTrack(self->track); self->track = NULL; - Py_CLEAR(self->mixer_obj); 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); @@ -847,6 +852,9 @@ 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; @@ -1001,12 +1009,6 @@ pg_track_obj_set_filestream(PGTrackObject *self, PyObject *args, Py_RETURN_NONE; } -#define SET_NUM_PROPERTY_IFNOTDEFAULT_ANDFLAG(props, property, value, \ - default, success) \ - if (value != default) { \ - success &= SDL_SetNumberProperty(props, property, value); \ - } - static PyObject * pg_track_obj_play(PGTrackObject *self, PyObject *args, PyObject *kwargs) { @@ -1048,9 +1050,11 @@ pg_track_obj_play(PGTrackObject *self, PyObject *args, PyObject *kwargs) 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; } diff --git a/src_py/_sdl3_mixer.py b/src_py/_sdl3_mixer.py index 7a3ba0befa..b0e73d41ed 100644 --- a/src_py/_sdl3_mixer.py +++ b/src_py/_sdl3_mixer.py @@ -72,7 +72,7 @@ def from_raw( ): if not isinstance(spec, audio.AudioSpec): raise TypeError( - f"Track 'spec' argument must be an AudioSpec, received {type(spec)}" + f"Audio 'spec' argument must be an AudioSpec, received {type(spec)}" ) return _sdl3_mixer_c.Audio.from_raw(