547 lines
14 KiB
Cython
547 lines
14 KiB
Cython
# Copyright 2004-2025 Tom Rothamel <pytom@bishoujo.us>
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person
|
|
# obtaining a copy of this software and associated documentation files
|
|
# (the "Software"), to deal in the Software without restriction,
|
|
# including without limitation the rights to use, copy, modify, merge,
|
|
# publish, distribute, sublicense, and/or sell copies of the Software,
|
|
# and to permit persons to whom the Software is furnished to do so,
|
|
# subject to the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be
|
|
# included in all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
|
|
"""
|
|
This file defines the API that Ren'Py uses to communicate with an audio and video
|
|
backend, and the default implementation of the API. This API is not intended
|
|
to be stable between multiple Ren'Py releases, and so is more intended for use
|
|
in ports to different platforms.
|
|
|
|
There are a few common variables with specific datatypes here.
|
|
|
|
`channel`
|
|
An integer giving the number of an audio channel. These integers are
|
|
allocated densely, but there's no limit to how many channels may be
|
|
present at one time.
|
|
|
|
`file`
|
|
This is an object representing an open file. This may be a Python file
|
|
object, or a Ren'Py subfile object. All objects past to this have a
|
|
`name` field, giving the name of the file. SubFiles also have `base`
|
|
and length. Base is the offset from the start of the file where the data
|
|
begins, while length is the amount of data in total.
|
|
|
|
Times and durations are represented as floats giving the number of seconds.
|
|
|
|
A channel may have up to two files associated with it, the playing file and
|
|
the queued file. The queued file begins playing when the current file ends
|
|
or is stopped.
|
|
"""
|
|
|
|
# When changing this API, change webaudio.py, too!
|
|
|
|
from __future__ import print_function
|
|
|
|
from libc.stdint cimport uintptr_t
|
|
|
|
from pygame_sdl2 cimport *
|
|
import_pygame_sdl2()
|
|
|
|
cdef extern from "renpysound_core.h":
|
|
|
|
void RPS_play(int channel, SDL_RWops *rw, char *ext, char* name, int synchro_start, int fadein, int tight, double start, double end, float volume, object audio_filter)
|
|
void RPS_queue(int channel, SDL_RWops *rw, char *ext, char *name, int synchro_start, int fadein, int tight, double start, double end, float volume, object audio_filter)
|
|
void RPS_stop(int channel)
|
|
void RPS_dequeue(int channel, int even_tight)
|
|
int RPS_queue_depth(int channel)
|
|
object RPS_playing_name(int channel)
|
|
void RPS_fadeout(int channel, int ms)
|
|
void RPS_pause(int channel, int pause)
|
|
void RPS_unpause_all_at_start()
|
|
void RPS_global_pause(int pause)
|
|
int RPS_get_pos(int channel)
|
|
double RPS_get_duration(int channel)
|
|
void RPS_set_endevent(int channel, int event)
|
|
void RPS_set_volume(int channel, float volume)
|
|
float RPS_get_volume(int channel)
|
|
void RPS_set_pan(int channel, float pan, float delay)
|
|
void RPS_set_secondary_volume(int channel, float vol2, float delay)
|
|
void RPS_replace_audio_filter(int channel, object audio_filter, int primary)
|
|
|
|
void RPS_advance_time()
|
|
int RPS_video_ready(int channel)
|
|
object RPS_read_video(int channel)
|
|
void RPS_set_video(int channel, int video)
|
|
|
|
void RPS_sample_surfaces(object, object)
|
|
void RPS_init(int freq, int stereo, int samples, int status, int equal_mono, int linear_fades)
|
|
void RPS_quit()
|
|
|
|
void RPS_periodic()
|
|
|
|
int RPS_get_sample_rate()
|
|
char *RPS_get_error()
|
|
|
|
void (*RPS_generate_audio_c_function)(float *stream, int length)
|
|
void (*RPS_apply_audio_filter)(object, float *stream, int length, int channels, int sample_rate)
|
|
|
|
|
|
from renpy.audio.filter cimport get_apply_audio_filter
|
|
RPS_apply_audio_filter = <void (*)(object, float *, int, int, int) noexcept> get_apply_audio_filter()
|
|
|
|
|
|
def check_error():
|
|
"""
|
|
This is called by Ren'Py to check for an error. This function should raise
|
|
a meaningful exception if an error has occurred in a background thread,
|
|
or do nothing if an error has not occured. (It should clear any error that
|
|
it raises.)
|
|
"""
|
|
|
|
e = RPS_get_error();
|
|
if len(e):
|
|
raise Exception(unicode(e, "utf-8", "replace"))
|
|
|
|
|
|
def play(channel, file, name, synchro_start=False, fadein=0, tight=False, start=0, end=0, relative_volume=1.0, audio_filter=None):
|
|
"""
|
|
Plays `file` on `channel`. This clears the playing and queued samples and
|
|
replaces them with this file.
|
|
|
|
`name`
|
|
A python object giving a readable name for the file.
|
|
|
|
`paused`
|
|
If True, playback is paused rather than started.
|
|
|
|
`fadein`
|
|
The time it should take the fade the music in, in seconds.
|
|
|
|
`tight`
|
|
If true, the file is played in tight mode. This means that fadeouts
|
|
can span between this file and the file queued after it.
|
|
|
|
`start`
|
|
A time in the file to start playing.
|
|
|
|
`end`
|
|
A time in the file to end playing. `
|
|
|
|
`relative_volume`
|
|
A float giving the relative volume of the file.
|
|
|
|
`audio_filter`
|
|
The audio filter to apply when the file is being played.
|
|
"""
|
|
|
|
cdef SDL_RWops *rw
|
|
|
|
if audio_filter is not None:
|
|
audio_filter.prepare(get_sample_rate())
|
|
|
|
rw = RWopsFromPython(file)
|
|
|
|
if rw == NULL:
|
|
raise Exception("Could not create RWops.")
|
|
|
|
if tight:
|
|
tight = 1
|
|
else:
|
|
tight = 0
|
|
|
|
name = name.encode("utf-8")
|
|
RPS_play(channel, rw, name, name, synchro_start, fadein * 1000, tight, start, end, relative_volume, audio_filter)
|
|
check_error()
|
|
|
|
|
|
def queue(channel, file, name, synchro_start=False, fadein=0, tight=False, start=0, end=0, relative_volume=1.0, audio_filter=None):
|
|
"""
|
|
Queues `file` on `channel` to play when the current file ends. If no file is
|
|
playing, plays it.
|
|
|
|
The other arguments are as for play.
|
|
"""
|
|
|
|
cdef SDL_RWops *rw
|
|
|
|
if audio_filter is not None:
|
|
audio_filter.prepare(get_sample_rate())
|
|
|
|
rw = RWopsFromPython(file)
|
|
|
|
if rw == NULL:
|
|
raise Exception("Could not create RWops.")
|
|
|
|
if tight:
|
|
tight = 1
|
|
else:
|
|
tight = 0
|
|
|
|
name = name.encode("utf-8")
|
|
RPS_queue(channel, rw, name, name, synchro_start, fadein * 1000, tight, start, end, relative_volume, audio_filter)
|
|
check_error()
|
|
|
|
|
|
def stop(channel):
|
|
"""
|
|
Immediately stops `channel`, and unqueues any queued audio file.
|
|
"""
|
|
|
|
RPS_stop(channel)
|
|
check_error()
|
|
|
|
|
|
def dequeue(channel, even_tight=False):
|
|
"""
|
|
Dequeues the queued sound file.
|
|
|
|
`even_tight`
|
|
If true, a queued sound file that is tight is not dequeued. If false,
|
|
a file marked as tight is dequeued.
|
|
"""
|
|
|
|
RPS_dequeue(channel, even_tight)
|
|
|
|
|
|
def queue_depth(channel):
|
|
"""
|
|
Returns the queue depth of the channel. 0 if no file is playing, 1 if
|
|
a files is playing but there is no queued file, and 2 if a file is playing
|
|
and one is queued.
|
|
"""
|
|
|
|
return RPS_queue_depth(channel)
|
|
|
|
|
|
def playing_name(channel):
|
|
"""
|
|
Returns the `name` argument of the playing sound. This was passed into
|
|
`play` or `queue`.
|
|
"""
|
|
|
|
rv = RPS_playing_name(channel)
|
|
|
|
if rv is not None:
|
|
rv = rv.decode("utf-8")
|
|
|
|
return rv
|
|
|
|
|
|
def pause(channel):
|
|
"""
|
|
Pauses `channel`.
|
|
"""
|
|
|
|
RPS_pause(channel, 1)
|
|
check_error()
|
|
|
|
|
|
def unpause(channel):
|
|
"""
|
|
Unpauses `channel`.
|
|
"""
|
|
|
|
RPS_pause(channel, 0)
|
|
check_error()
|
|
|
|
|
|
def global_pause(pause):
|
|
"""
|
|
Pauses or unpauses all channels.
|
|
"""
|
|
|
|
RPS_global_pause(pause)
|
|
|
|
|
|
def fadeout(channel, delay):
|
|
"""
|
|
Fades out `channel` over `delay` seconds.
|
|
"""
|
|
|
|
RPS_fadeout(channel, int(delay * 1000))
|
|
check_error()
|
|
|
|
|
|
def busy(channel):
|
|
"""
|
|
Returns true if `channel` is currently playing something, and false
|
|
otherwise
|
|
"""
|
|
|
|
return RPS_get_pos(channel) != -1
|
|
|
|
|
|
def get_pos(channel):
|
|
"""
|
|
Returns the position of the audio file playing in `channel`, in seconds.
|
|
Returns None if not file is is playing or it is not known.
|
|
"""
|
|
|
|
return RPS_get_pos(channel) / 1000.0
|
|
|
|
|
|
def get_duration(channel):
|
|
"""
|
|
Returns the duration of the audio file playing in `channel`, in seconds, or
|
|
None if no file is playing or it is not known.
|
|
"""
|
|
|
|
return RPS_get_duration(channel)
|
|
|
|
|
|
def set_volume(channel, volume):
|
|
"""
|
|
Sets the primary volume for `channel` to `volume`, a number between
|
|
0 and 1. This volume control is linear.
|
|
"""
|
|
|
|
if volume == 0:
|
|
RPS_set_volume(channel, 0)
|
|
else:
|
|
RPS_set_volume(channel, volume)
|
|
|
|
check_error()
|
|
|
|
|
|
def set_pan(channel, pan, delay):
|
|
"""
|
|
Sets the pan for channel.
|
|
|
|
`pan`
|
|
A number between -1 and 1 that control the placement of the audio.
|
|
If this is -1, then all audio is sent to the left channel.
|
|
If it's 0, then the two channels are equally balanced. If it's 1,
|
|
then all audio is sent to the right ear.
|
|
|
|
`delay`
|
|
The amount of time it takes for the panning to occur.
|
|
"""
|
|
|
|
RPS_set_pan(channel, pan, delay)
|
|
check_error()
|
|
|
|
|
|
def set_secondary_volume(channel, volume, delay):
|
|
"""
|
|
Sets the secondary volume for channel. This is linear, and is multiplied
|
|
with the primary volume and scale factors derived from pan to find the
|
|
actual multiplier used on the samples.
|
|
|
|
`delay`
|
|
The time it takes for the change in volume to happen.
|
|
"""
|
|
|
|
RPS_set_secondary_volume(channel, volume, delay)
|
|
check_error()
|
|
|
|
|
|
def replace_audio_filter(channel, audio_filter, playing):
|
|
"""
|
|
Replaces the audio filter for `channel` with `audio_filter`.
|
|
|
|
`playing`
|
|
If true, the filter is applied to the currently playing file and queued file. If false,
|
|
the filter is only applied to the queued file.
|
|
"""
|
|
|
|
if audio_filter is not None:
|
|
audio_filter.prepare(get_sample_rate())
|
|
|
|
RPS_replace_audio_filter(channel, audio_filter, playing)
|
|
|
|
|
|
def deallocate_audio_filter(audio_filter):
|
|
"""
|
|
Called when an audio filter is about to be dealloica to release all
|
|
assocated resources.
|
|
"""
|
|
|
|
# Does nothing on this backend, but is used on web audio.
|
|
|
|
|
|
def get_volume(channel):
|
|
"""
|
|
Gets the primary volume associated with `channel`.
|
|
"""
|
|
|
|
return RPS_get_volume(channel)
|
|
|
|
|
|
def video_ready(channel):
|
|
"""
|
|
Returns true if the video playing on `channel` has a frame ready for
|
|
presentation.
|
|
"""
|
|
|
|
return RPS_video_ready(channel)
|
|
|
|
|
|
def read_video(channel):
|
|
"""
|
|
Returns the frame of video playing on `channel`. This should be returned
|
|
as an SDL surface with 2px of padding on all sides.
|
|
"""
|
|
|
|
rv = RPS_read_video(channel)
|
|
|
|
if rv is None:
|
|
return rv
|
|
|
|
# Remove padding from the edges of the surface.
|
|
w, h = rv.get_size()
|
|
|
|
# This has to be set to the same number it is in ffmedia.c
|
|
FRAME_PADDING = 4
|
|
return rv.subsurface((FRAME_PADDING, FRAME_PADDING, w - FRAME_PADDING * 2, h - FRAME_PADDING * 2))
|
|
|
|
|
|
# No video will be played from this channel.
|
|
NO_VIDEO = 0
|
|
|
|
# The video will be played while avoiding framedrops.
|
|
NODROP_VIDEO = 1
|
|
|
|
# The video will be played, allowing framedrops.
|
|
DROP_VIDEO = 2
|
|
|
|
def set_video(channel, video, loop=False):
|
|
"""
|
|
Sets a flag that determines if this channel will attempt to decode video.
|
|
|
|
`video`
|
|
One of `NO_VIDEO`, `NODROP_VIDEO`, or `DROP_VIDEO`.
|
|
|
|
`loop`
|
|
If true, the video file will loop.
|
|
"""
|
|
|
|
if video == NODROP_VIDEO:
|
|
RPS_set_video(channel, NODROP_VIDEO)
|
|
elif video:
|
|
RPS_set_video(channel, DROP_VIDEO)
|
|
else:
|
|
RPS_set_video(channel, NO_VIDEO)
|
|
|
|
|
|
def init(freq, stereo, samples, status=False, equal_mono=False, linear_fades=False):
|
|
"""
|
|
Initializes the audio system with the given parameters. The parameter are
|
|
just informational - the audio system should be able to play all supported
|
|
files.
|
|
|
|
`freq`
|
|
The sample frequency to play back at.
|
|
|
|
`stereo`
|
|
Should we play in stereo (generally true).
|
|
|
|
`samples`
|
|
The length of the sample buffer.
|
|
|
|
`status`
|
|
If true, the sound system will print errors out to the console.
|
|
|
|
`equal_mono`
|
|
If true, the sound system will play mono files at the same volume as
|
|
stereo files.
|
|
|
|
`linear_fades`
|
|
If true, the sound system will use linear fades.
|
|
"""
|
|
|
|
if status:
|
|
status = 1
|
|
else:
|
|
status = 0
|
|
|
|
RPS_init(freq, stereo, samples, status, equal_mono, linear_fades)
|
|
check_error()
|
|
|
|
|
|
def quit(): # @ReservedAssignment
|
|
"""
|
|
De-initializes the audio system.
|
|
"""
|
|
|
|
RPS_quit()
|
|
|
|
|
|
def periodic():
|
|
"""
|
|
Called periodically (at 20 Hz).
|
|
"""
|
|
|
|
RPS_periodic()
|
|
|
|
|
|
def advance_time():
|
|
"""
|
|
Called to advance time at the start of a frame.
|
|
"""
|
|
|
|
RPS_advance_time()
|
|
|
|
|
|
def get_sample_rate():
|
|
"""
|
|
Returns the sample rate of the audio system.
|
|
|
|
This is not part of the api shared with other audio implementations
|
|
"""
|
|
|
|
return RPS_get_sample_rate()
|
|
|
|
|
|
def set_generate_audio_c_function(fn):
|
|
"""
|
|
This can be use to set a C function that totally replaces the Ren'Py
|
|
audio system.
|
|
|
|
The function is expected to have the signature void (*)(short *buf, int samples,
|
|
and fill buf with samples samples of audio, where each sample consists of two
|
|
shorts.
|
|
|
|
This is not part of the api shared with other audio implementations
|
|
"""
|
|
|
|
global RPS_generate_audio_c_function
|
|
|
|
if not isinstance(fn, int):
|
|
import ctypes
|
|
fn = ctypes.cast(fn, ctypes.c_void_p).value
|
|
|
|
RPS_generate_audio_c_function = <void (*)(float *, int) noexcept> <uintptr_t> fn
|
|
|
|
|
|
# Store the sample surfaces so they stay alive.
|
|
rgb_surface = None
|
|
rgba_surface = None
|
|
|
|
def sample_surfaces(rgb, rgba):
|
|
"""
|
|
Called to provide sample surfaces to the display system. The surfaces
|
|
returned by read_video should be in the same format as these.
|
|
"""
|
|
|
|
global rgb_surface
|
|
global rgba_surface
|
|
|
|
rgb_surface = rgb
|
|
rgba_surface = rgb
|
|
|
|
RPS_sample_surfaces(rgb, rgba)
|
|
|
|
|
|
# Is this the webaudio module?
|
|
is_webaudio = False
|
|
|
|
# When changing this API, change webaudio.py, too!
|