586 lines
15 KiB
Python
586 lines
15 KiB
Python
# Copyright 2004-2019 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.
|
|
|
|
# The public API for music in games.
|
|
|
|
from __future__ import print_function
|
|
|
|
import renpy.audio
|
|
|
|
from renpy.audio.audio import get_channel, get_serial
|
|
|
|
# Part of the public api:
|
|
from renpy.audio.audio import register_channel, alias_channel
|
|
register_channel; alias_channel
|
|
|
|
|
|
def play(filenames, channel="music", loop=None, fadeout=None, synchro_start=False, fadein=0, tight=None, if_changed=False):
|
|
"""
|
|
:doc: audio
|
|
|
|
This stops the music currently playing on the numbered channel, dequeues
|
|
any queued music, and begins playing the specified file or files.
|
|
|
|
`filenames`
|
|
This may be a single file, or a list of files to be played.
|
|
|
|
`channel`
|
|
The channel to play the sound on.
|
|
|
|
`loop`
|
|
If this is True, the tracks will loop while they are the last thing
|
|
in the queue.
|
|
|
|
`fadeout`
|
|
If not None, this is a time in seconds to fade for. Otherwise the
|
|
fadeout time is taken from config.fade_music.
|
|
|
|
`synchro_start`
|
|
Ren'Py will ensure that all channels of with synchro_start set to true
|
|
will start playing at exactly the same time. Synchro_start should be
|
|
true when playing two audio files that are meant to be synchronized
|
|
with each other.
|
|
|
|
`fadein`
|
|
This is the number of seconds to fade the music in for, on the
|
|
first loop only.
|
|
|
|
`tight`
|
|
If this is True, then fadeouts will span into the next-queued sound. If
|
|
None, this is true when loop is True, and false otherwise.
|
|
|
|
`if_changed`
|
|
If this is True, and the music file is currently playing,
|
|
then it will not be stopped/faded out and faded back in again, but
|
|
instead will be kept playing. (This will always queue up an additional
|
|
loop of the music.)
|
|
|
|
This clears the pause flag for `channel`.
|
|
"""
|
|
|
|
if renpy.game.context().init_phase:
|
|
raise Exception("Can't play music during init phase.")
|
|
|
|
if filenames is None:
|
|
return
|
|
|
|
if isinstance(filenames, basestring):
|
|
filenames = [ filenames ]
|
|
|
|
with renpy.audio.audio.lock:
|
|
|
|
try:
|
|
c = get_channel(channel)
|
|
ctx = c.context
|
|
|
|
if loop is None:
|
|
loop = c.default_loop
|
|
|
|
if (tight is None) and renpy.config.tight_loop_default:
|
|
tight = loop
|
|
|
|
loop_is_filenames = (c.loop == filenames)
|
|
|
|
c.dequeue()
|
|
|
|
if fadeout is None:
|
|
fadeout = renpy.config.fade_music
|
|
|
|
if if_changed and c.get_playing() in filenames:
|
|
fadein = 0
|
|
loop_only = loop_is_filenames
|
|
else:
|
|
c.fadeout(fadeout)
|
|
loop_only = False
|
|
|
|
if renpy.config.skip_sounds and renpy.config.skipping and (not loop):
|
|
enqueue = False
|
|
else:
|
|
enqueue = True
|
|
|
|
if enqueue:
|
|
c.enqueue(filenames, loop=loop, synchro_start=synchro_start, fadein=fadein, tight=tight, loop_only=loop_only)
|
|
|
|
t = get_serial()
|
|
ctx.last_changed = t
|
|
c.last_changed = t
|
|
|
|
if loop:
|
|
ctx.last_filenames = filenames
|
|
ctx.last_tight = tight
|
|
else:
|
|
ctx.last_filenames = [ ]
|
|
ctx.last_tight = False
|
|
|
|
ctx.pause = False
|
|
|
|
except:
|
|
if renpy.config.debug_sound:
|
|
raise
|
|
|
|
|
|
def queue(filenames, channel="music", loop=None, clear_queue=True, fadein=0, tight=None):
|
|
"""
|
|
:doc: audio
|
|
|
|
This queues the given filenames on the specified channel.
|
|
|
|
`filenames`
|
|
This may be a single file, or a list of files to be played.
|
|
|
|
`channel`
|
|
The channel to play the sound on.
|
|
|
|
`loop`
|
|
If this is True, the tracks will loop while they are the last thing
|
|
in the queue.
|
|
|
|
`clear_queue`
|
|
If True, then the queue is cleared, making these files the files that
|
|
are played when the currently playing file finishes. If it is False,
|
|
then these files are placed at the back of the queue. In either case,
|
|
if no music is playing these files begin playing immediately.
|
|
|
|
`fadein`
|
|
This is the number of seconds to fade the music in for, on the
|
|
first loop only.
|
|
|
|
`tight`
|
|
If this is True, then fadeouts will span into the next-queued sound. If
|
|
None, this is true when loop is True, and false otherwise.
|
|
|
|
This clears the pause flag for `channel`.
|
|
"""
|
|
|
|
if renpy.game.context().init_phase:
|
|
raise Exception("Can't play music during init phase.")
|
|
|
|
if filenames is None:
|
|
filenames = [ ]
|
|
loop = False
|
|
|
|
if isinstance(filenames, basestring):
|
|
filenames = [ filenames ]
|
|
|
|
if renpy.config.skipping == "fast":
|
|
stop(channel)
|
|
|
|
with renpy.audio.audio.lock:
|
|
|
|
try:
|
|
|
|
c = get_channel(channel)
|
|
ctx = c.context
|
|
|
|
if loop is None:
|
|
loop = c.default_loop
|
|
|
|
if (tight is None) and renpy.config.tight_loop_default:
|
|
tight = loop
|
|
|
|
if clear_queue:
|
|
c.dequeue(True)
|
|
|
|
if renpy.config.skip_sounds and renpy.config.skipping and (not loop):
|
|
enqueue = False
|
|
else:
|
|
enqueue = True
|
|
|
|
if enqueue:
|
|
c.enqueue(filenames, loop=loop, fadein=fadein, tight=tight)
|
|
|
|
t = get_serial()
|
|
ctx.last_changed = t
|
|
c.last_changed = t
|
|
|
|
if loop:
|
|
ctx.last_filenames = filenames
|
|
ctx.last_tight = tight
|
|
else:
|
|
ctx.last_filenames = [ ]
|
|
ctx.last_tight = False
|
|
|
|
ctx.pause = False
|
|
|
|
except:
|
|
if renpy.config.debug_sound:
|
|
raise
|
|
|
|
|
|
def playable(filename, channel="music"):
|
|
"""
|
|
Return true if the given filename is playable on the channel. This
|
|
takes into account the prefix and suffix, and ignores a preceding
|
|
specifier.
|
|
"""
|
|
|
|
c = get_channel(channel)
|
|
|
|
filename, _, _ = c.split_filename(filename, False)
|
|
|
|
return renpy.loader.loadable(c.file_prefix + filename + c.file_suffix)
|
|
|
|
|
|
def stop(channel="music", fadeout=None):
|
|
"""
|
|
:doc: audio
|
|
|
|
This stops the music that is currently playing, and dequeues all
|
|
queued music. If fadeout is None, the music is faded out for the
|
|
time given in config.fade_music, otherwise it is faded for fadeout
|
|
seconds.
|
|
|
|
This sets the last queued file to None.
|
|
|
|
`channel`
|
|
The channel to stop the sound on.
|
|
|
|
`fadeout`
|
|
If not None, this is a time in seconds to fade for. Otherwise the
|
|
fadeout time is taken from config.fade_music.
|
|
|
|
|
|
"""
|
|
|
|
if renpy.game.context().init_phase:
|
|
return
|
|
|
|
with renpy.audio.audio.lock:
|
|
|
|
try:
|
|
c = get_channel(channel)
|
|
ctx = c.context
|
|
|
|
if fadeout is None:
|
|
fadeout = renpy.config.fade_music
|
|
|
|
c.fadeout(fadeout)
|
|
|
|
t = get_serial()
|
|
ctx.last_changed = t
|
|
c.last_changed = t
|
|
ctx.last_filenames = [ ]
|
|
ctx.last_tight = False
|
|
|
|
except:
|
|
if renpy.config.debug_sound:
|
|
raise
|
|
|
|
|
|
def set_music(channel, flag, default=False):
|
|
"""
|
|
Determines if channel will loop by default.
|
|
"""
|
|
|
|
c = get_channel(channel)
|
|
|
|
if default and c.default_loop_set:
|
|
return
|
|
|
|
c.default_loop = flag
|
|
c.default_loop_set = True
|
|
|
|
|
|
def is_music(channel):
|
|
"""
|
|
Returns true if "channel" will loop by default.
|
|
"""
|
|
|
|
c = get_channel(channel)
|
|
return c.default_loop
|
|
|
|
|
|
def get_delay(time, channel="music"):
|
|
"""
|
|
Returns the number of seconds left until the given time in the
|
|
music.
|
|
"""
|
|
|
|
try:
|
|
c = renpy.audio.audio.get_channel(channel)
|
|
t = c.get_pos()
|
|
|
|
if not t or t < 0:
|
|
return None
|
|
|
|
if t > time:
|
|
return 0
|
|
|
|
return time - t
|
|
|
|
except:
|
|
if renpy.config.debug_sound:
|
|
raise
|
|
|
|
return None
|
|
|
|
|
|
def get_pos(channel="music"):
|
|
"""
|
|
:doc: audio
|
|
|
|
Returns the current position of the audio or video file on `channel`, in
|
|
seconds. Returns None if no audio is playing on `channel`.
|
|
|
|
As this may return None before a channel starts playing, or if the audio
|
|
channel involved has been muted, callers of this function should
|
|
always handle a None value.
|
|
"""
|
|
|
|
try:
|
|
c = renpy.audio.audio.get_channel(channel)
|
|
t = c.get_pos()
|
|
|
|
if not t or t < 0:
|
|
return None
|
|
|
|
return t / 1000.0
|
|
|
|
except:
|
|
if renpy.config.debug_sound:
|
|
raise
|
|
|
|
return None
|
|
|
|
|
|
def get_duration(channel="music"):
|
|
"""
|
|
:doc: audio
|
|
|
|
Returns the duration of the audio or video file on `channel`. Returns
|
|
0.0 if no file is playing on `channel`.
|
|
"""
|
|
|
|
try:
|
|
c = renpy.audio.audio.get_channel(channel)
|
|
return c.get_duration()
|
|
|
|
except:
|
|
if renpy.config.debug_sound:
|
|
raise
|
|
|
|
return None
|
|
|
|
|
|
def get_playing(channel="music"):
|
|
"""
|
|
:doc: audio
|
|
|
|
If the given channel is playing, returns the playing file name.
|
|
Otherwise, returns None.
|
|
"""
|
|
|
|
try:
|
|
c = renpy.audio.audio.get_channel(channel)
|
|
return c.get_playing()
|
|
except:
|
|
if renpy.config.debug_sound:
|
|
raise
|
|
|
|
return None
|
|
|
|
|
|
def is_playing(channel="music"):
|
|
"""
|
|
:doc: audio
|
|
|
|
Returns True if the channel is currently playing a sound, False if
|
|
it is not, or if the sound system isn't working.
|
|
"""
|
|
|
|
return (get_playing(channel=channel) is not None)
|
|
|
|
|
|
def set_volume(volume, delay=0, channel="music"):
|
|
"""
|
|
:doc: audio
|
|
|
|
Sets the volume of this channel, as a fraction of the volume of the
|
|
mixer controlling the channel.
|
|
|
|
`volume`
|
|
This is a number between 0.0 and 1.0, and is interpreted as a fraction
|
|
of the mixer volume for the channel.
|
|
|
|
`delay`
|
|
It takes delay seconds to change/fade the volume from the old to
|
|
the new value. This value is persisted into saves, and participates
|
|
in rollback.
|
|
|
|
`channel`
|
|
The channel to be set
|
|
"""
|
|
|
|
try:
|
|
c = renpy.audio.audio.get_channel(channel)
|
|
c.set_secondary_volume(volume, delay)
|
|
except:
|
|
if renpy.config.debug_sound:
|
|
raise
|
|
|
|
|
|
def set_pan(pan, delay, channel="music"):
|
|
"""
|
|
:doc: audio
|
|
|
|
Sets the pan of this 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.
|
|
|
|
`channel`
|
|
The channel the panning takes place on. This can be a sound or a music
|
|
channel. Often, this is channel 7, the default music channel.
|
|
"""
|
|
|
|
try:
|
|
c = renpy.audio.audio.get_channel(channel)
|
|
c.set_pan(pan, delay)
|
|
except:
|
|
if renpy.config.debug_sound:
|
|
raise
|
|
|
|
|
|
def set_queue_empty_callback(callback, channel="music"):
|
|
"""
|
|
:doc: audio
|
|
|
|
This sets a callback that is called when the queue is empty. This
|
|
callback is called when the queue first becomes empty, and at
|
|
least once per interaction while the queue is empty.
|
|
|
|
The callback is called with no parameters. It can queue sounds by
|
|
calling renpy.music.queue with the appropriate arguments. Please
|
|
note that the callback may be called while a sound is playing, as
|
|
long as a queue slot is empty.
|
|
"""
|
|
try:
|
|
c = renpy.audio.audio.get_channel(channel)
|
|
c.callback = callback
|
|
except:
|
|
if renpy.config.debug_sound:
|
|
raise
|
|
|
|
|
|
def set_pause(value, channel="music"):
|
|
"""
|
|
:doc: audio
|
|
|
|
Sets the pause flag for `channel` to `value`. If True, the channel
|
|
will pause, otherwise it will play normally.
|
|
"""
|
|
try:
|
|
c = renpy.audio.audio.get_channel(channel)
|
|
c.context.pause = value
|
|
except:
|
|
if renpy.config.debug_sound:
|
|
raise
|
|
|
|
|
|
def get_pause(channel="music"):
|
|
"""
|
|
:doc: audio
|
|
|
|
Returns the pause flag for `channel`.
|
|
"""
|
|
try:
|
|
c = renpy.audio.audio.get_channel(channel)
|
|
return c.context.pause
|
|
except:
|
|
|
|
return False
|
|
|
|
|
|
def set_mixer(channel, mixer, default=False):
|
|
"""
|
|
This sets the name of the mixer associated with a given
|
|
channel. By default, there are two mixers, 'sfx' and
|
|
'music'. 'sfx' is on channels 0 to 3, and 'music'
|
|
on 3 to 7. The voice module calls this function to set channel 2 to voice.
|
|
You can create your own mixer, but will need to add a preference if you
|
|
wish to allow the user to set it.
|
|
|
|
This function should only be called in an init block.
|
|
"""
|
|
|
|
try:
|
|
c = renpy.audio.audio.get_channel(channel)
|
|
|
|
if not default or c.mixer is None:
|
|
c.mixer = mixer
|
|
|
|
except:
|
|
if renpy.config.debug_sound:
|
|
raise
|
|
|
|
|
|
def get_all_mixers():
|
|
"""
|
|
This gets all mixers in use.
|
|
"""
|
|
|
|
rv = set()
|
|
|
|
for i in renpy.audio.audio.all_channels:
|
|
rv.add(i.mixer)
|
|
|
|
return list(rv)
|
|
|
|
|
|
def channel_defined(channel):
|
|
"""
|
|
Returns True if the channel exists, or False otherwise.
|
|
"""
|
|
|
|
try:
|
|
renpy.audio.audio.get_channel(channel)
|
|
return True
|
|
except:
|
|
return False
|
|
|
|
|
|
# Music change logic:
|
|
|
|
# Use the queueing time to determine what should or should not be
|
|
# queued
|
|
|
|
# m_filenames - music filenames from info object
|
|
# m_loop - music loop from info object
|
|
# c_filenames - music filenames from channel
|
|
# c_filenames - music loop from channel
|
|
|
|
# if m_filenames == c_filenames and m_loop == c_loop:
|
|
# do nothing, the music is right.
|
|
|
|
# otherwise,
|
|
# dequeue music from the channel.
|
|
|
|
# if m_filenames != c_playing_filenames:
|
|
# stop the music with fade. The music is wrong, change it.
|
|
|
|
# if m_loop:
|
|
# queue m_filenames looping
|