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',