Permalink
Cannot retrieve contributors at this time
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
renpy/renpy/common/00voice.rpy
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
586 lines (429 sloc)
17.1 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Copyright 2004-2022 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 extra contains a basic implementation of voice support. Right | |
| # now, voice is given its own toggle, and can either be turned on or | |
| # turned off. In the future, we'll probably provide some way of | |
| # toggling it on or off for individual characters. | |
| # | |
| # To use it, place a voice "<sndfile>" line before each voiced line of | |
| # dialogue. | |
| # | |
| # voice "e_1001.ogg" | |
| # e "Voice support lets you add the spoken word to your games." | |
| # | |
| # Normally, a voice is cancelled at the start of the next | |
| # interaction. If you want a voice to span interactions, call | |
| # voice_sustain. | |
| # | |
| # voice "e_1002.ogg" | |
| # e "Voice sustain is a technique that allows the same voice file.." | |
| # | |
| # $ voice_sustain() | |
| # e "...to play for two lines of dialogue." | |
| init -1500 python: | |
| _voice = object() | |
| _voice.play = None | |
| _voice.sustain = False | |
| _voice.seen_in_lint = False | |
| _voice.tag = None | |
| _voice.tlid = None | |
| _voice.auto_file = None | |
| _voice.info = None | |
| _voice.last_playing = 0.0 | |
| # If true, the voice system ignores the interaction. | |
| _voice.ignore_interaction = False | |
| # The voice filename format. This may contain the voice tag | |
| config.voice_filename_format = "{filename}" | |
| # This is formatted with {id} to produce a filename. If the filename | |
| # exists, it's played as a voice file. | |
| config.auto_voice = None | |
| # The last sound played on the voice channel. (This is used to replay | |
| # it.) | |
| _last_voice_play = None | |
| # Call this to specify the voice file that will be played for | |
| # the user. This peice only gathers the information so | |
| # voice_interact can play the right file. | |
| def voice(filename, tag=None): | |
| """ | |
| :doc: voice | |
| Plays `filename` on the voice channel. The equivalent of the voice | |
| statement. | |
| `filename` | |
| The filename to play. This is used with | |
| :var:`config.voice_filename_format` to produce the | |
| filename that will be played. | |
| `tag` | |
| If this is not None, it should be a string giving a | |
| voice tag to be played. If None, this takes its | |
| default value from the voice_tag of the Character | |
| that causes the next interaction. | |
| The voice tag is used to specify which character is | |
| speaking, to allow a user to mute or unmute the | |
| voices of particular characters. | |
| """ | |
| if not config.has_voice: | |
| return | |
| fn = config.voice_filename_format.format(filename=filename) | |
| _voice.play = fn | |
| _voice.tag = tag | |
| # Call this to specify that the currently playing voice file | |
| # should be sustained through the current interaction. | |
| def voice_sustain(ignored="", **kwargs): | |
| """ | |
| :doc: voice | |
| The equivalent of the voice sustain statement. | |
| """ | |
| if not config.has_voice: | |
| return | |
| _voice.sustain = True | |
| # Call this to replay the last bit of voice. | |
| def voice_replay(): | |
| """ | |
| :doc: voice | |
| Replays the current voice, if possible. | |
| """ | |
| if _last_voice_play is not None: | |
| renpy.sound.play(_last_voice_play, channel="voice") | |
| # Returns true if we can replay the voice. | |
| def voice_can_replay(): | |
| """ | |
| :doc: voice | |
| Returns true if it's possible to replay the current voice. | |
| """ | |
| return _last_voice_play is not None | |
| @renpy.pure | |
| class SetVoiceMute(Action, DictEquality): | |
| """ | |
| :doc: voice_action | |
| If `mute` is true, mutes voices that are played with the given | |
| `voice_tag`. If `mute` is false, unmutes voices that are played | |
| with `voice_tag`. | |
| """ | |
| def __init__(self, voice_tag, mute): | |
| self.voice_tag = voice_tag | |
| self.mute = mute | |
| def get_selected(self): | |
| if self.mute: | |
| return self.voice_tag in persistent._voice_mute | |
| else: | |
| return self.voice_tag not in persistent._voice_mute | |
| def __call__(self): | |
| if self.mute: | |
| persistent._voice_mute.add(self.voice_tag) | |
| else: | |
| persistent._voice_mute.discard(self.voice_tag) | |
| renpy.restart_interaction() | |
| @renpy.pure | |
| def SetCharacterVolume(voice_tag, volume=None): | |
| """ | |
| :doc: voice_action | |
| This allows the volume of each characters to be adjusted. | |
| If `volume` is None, this returns a BarValue that | |
| controls the value of `voice_tag`. Otherwise, this set it to `volume`. | |
| `volume` is a number between 0.0 and 1.0, and is interpreted as a | |
| fraction of the mixer volume for `voice` channel. | |
| """ | |
| if voice_tag not in persistent._character_volume: | |
| persistent._character_volume[voice_tag] = 1.0 | |
| if volume is None: | |
| return _CharacterVolumeValue(voice_tag) | |
| else: | |
| if config.quadratic_volume: | |
| return SetDict(persistent._character_volume, voice_tag, volume ** 2) | |
| else: | |
| return SetDict(persistent._character_volume, voice_tag, volume) | |
| def GetCharacterVolume(voice_tag): | |
| """ | |
| :doc: preference_functions | |
| This returns the volume associated with voice tag, a number | |
| between 0.0 and 1.0, which is interpreted as a fraction of the | |
| mixer volume for the `voice` channel. | |
| """ | |
| return persistent._character_volume.get(voice_tag, 1.0) | |
| @renpy.pure | |
| class PlayCharacterVoice(Action, FieldEquality): | |
| """ | |
| :doc: voice_action | |
| This plays `sample` on the voice channel, as if said by a | |
| character with `voice_tag`. | |
| `sample` | |
| The full path to a sound file. No voice-related handling | |
| of this file is done. | |
| `selected` | |
| If True, buttons using this action will be marked as selected | |
| while the sample is playing. | |
| """ | |
| equality_fields = [ "voice_tag", "sample", "can_be_selected" ] | |
| can_be_selected = False | |
| selected = False | |
| def __init__(self, voice_tag, sample, selected=False): | |
| self.voice_tag = voice_tag | |
| self.sample = sample | |
| self.can_be_selected = selected | |
| def __call__(self): | |
| if self.voice_tag in persistent._voice_mute: | |
| return | |
| volume = persistent._character_volume.get(self.voice_tag, 1.0) | |
| renpy.music.get_channel("voice").set_volume(volume) | |
| renpy.sound.play(self.sample, channel="voice") | |
| renpy.restart_interaction() | |
| self.periodic(0) | |
| def get_selected(self): | |
| if not self.can_be_selected: | |
| return False | |
| return renpy.sound.get_playing(channel="voice") == self.sample | |
| def periodic(self, st): | |
| if not self.can_be_selected: | |
| return None | |
| old_selected = self.selected | |
| new_selected = self.get_selected() | |
| if old_selected != new_selected: | |
| renpy.restart_interaction() | |
| self.selected = new_selected | |
| return .1 | |
| @renpy.pure | |
| class ToggleVoiceMute(Action, DictEquality): | |
| """ | |
| :doc: voice_action | |
| Toggles the muting of `voice_tag`. This is selected if | |
| the given voice tag is muted, unless `invert` is true, | |
| in which case it's selected if the voice is unmuted. | |
| """ | |
| def __init__(self, voice_tag, invert=False): | |
| self.voice_tag = voice_tag | |
| self.invert = invert | |
| def get_selected(self): | |
| rv = self.voice_tag in persistent._voice_mute | |
| if self.invert: | |
| return not rv | |
| else: | |
| return rv | |
| def __call__(self): | |
| if self.voice_tag not in persistent._voice_mute: | |
| persistent._voice_mute.add(self.voice_tag) | |
| else: | |
| persistent._voice_mute.discard(self.voice_tag) | |
| renpy.restart_interaction() | |
| @renpy.pure | |
| class VoiceReplay(Action, DictEquality): | |
| """ | |
| :doc: voice_action | |
| Replays the most recently played voice. | |
| """ | |
| def __call__(self): | |
| voice_replay() | |
| def get_sensitive(self): | |
| return voice_can_replay() | |
| class VoiceInfo(_object): | |
| """ | |
| An object returned by VoiceInfo and get_voice_info(). | |
| """ | |
| def __init__(self): | |
| self.filename = _voice.play | |
| self.auto_filename = None | |
| self.tlid = None | |
| self.sustain = _voice.sustain | |
| self.tag = _voice.tag | |
| if not self.filename and config.auto_voice: | |
| for tlid in [ | |
| renpy.game.context().translate_identifier, | |
| renpy.game.context().alternate_translate_identifier, | |
| renpy.game.context().deferred_translate_identifier, | |
| ]: | |
| if tlid is None: | |
| continue | |
| if isinstance(config.auto_voice, (str, unicode)): | |
| fn = config.auto_voice.format(id=tlid) | |
| else: | |
| fn = config.auto_voice(tlid) | |
| self.auto_filename = fn | |
| if fn and renpy.loadable(fn): | |
| if _voice.tlid == tlid: | |
| self.sustain = True | |
| else: | |
| self.filename = fn | |
| break | |
| self.tlid = renpy.game.context().translate_identifier or renpy.game.context().deferred_translate_identifier | |
| if self.filename: | |
| self.sustain = False | |
| elif self.sustain and (self.sustain != "preference"): | |
| self.filename = _last_voice_play | |
| def _get_voice_info(): | |
| """ | |
| :doc: voice | |
| Returns information about the voice being played by the current | |
| say statement. This function may only be called while a say statement | |
| is executing. | |
| The object returned has the following fields: | |
| .. attribute:: VoiceInfo.filename | |
| The filename of the voice to be played, or None if no files | |
| should be played. | |
| .. attribute:: VoiceInfo.auto_filename | |
| The filename that Ren'Py looked in for automatic-voicing | |
| purposes, or None if one could not be found. | |
| .. attribute:: VoiceInfo.tag | |
| The voice_tag parameter supplied to the speaking Character. | |
| .. attribute:: VoiceInfo.sustain | |
| False if the file was played as part of this interaction. True if | |
| it was sustained from a previous interaction. | |
| """ | |
| vi = VoiceInfo() | |
| if _voice.info is None: | |
| return vi | |
| elif _voice.info.tlid == vi.tlid: | |
| return _voice.info | |
| else: | |
| return vi | |
| def _voice_history_callback(h): | |
| h.voice = _get_voice_info() | |
| config.history_callbacks.append(_voice_history_callback) | |
| init -1500 python hide: | |
| # basics: True if the game will have voice. | |
| config.has_voice = True | |
| # The set of voice tags that are currently muted. | |
| if persistent._voice_mute is None: | |
| persistent._voice_mute = set() | |
| # The dictionary of the volume of each voice tags. | |
| if persistent._character_volume is None: | |
| persistent._character_volume = dict() | |
| # This is called on each interaction, to ensure that the | |
| # appropriate voice file is played for the user. | |
| def voice_interact(): | |
| if not config.has_voice: | |
| return | |
| if _voice.ignore_interaction: | |
| return | |
| mode = renpy.get_mode() | |
| if (mode is None) or (mode == "with"): | |
| return | |
| if getattr(renpy.context(), "_menu", False): | |
| renpy.sound.stop(channel="voice") | |
| return | |
| if _preferences.voice_sustain and not _voice.sustain: | |
| _voice.sustain = "preference" | |
| if _voice.play: | |
| _voice.sustain = False | |
| vi = VoiceInfo() | |
| if not _voice.sustain: | |
| _voice.info = vi | |
| if not vi.sustain: | |
| _voice.play = vi.filename | |
| else: | |
| _voice.play = None | |
| renpy.game.context().deferred_translate_identifier = None | |
| _voice.auto_file = vi.auto_filename | |
| _voice.sustain = vi.sustain | |
| _voice.tlid = vi.tlid | |
| volume = persistent._character_volume.get(_voice.tag, 1.0) | |
| if (not volume) or (_voice.tag in persistent._voice_mute): | |
| renpy.sound.stop(channel="voice") | |
| store._last_voice_play = _voice.play | |
| elif _voice.play: | |
| if not config.skipping: | |
| renpy.music.get_channel("voice").set_volume(volume) | |
| renpy.sound.play(_voice.play, channel="voice") | |
| store._last_voice_play = _voice.play | |
| elif not _voice.sustain: | |
| renpy.sound.stop(channel="voice") | |
| if not getattr(renpy.context(), "_menu", False): | |
| store._last_voice_play = None | |
| _voice.play = None | |
| _voice.sustain = False | |
| _voice.tag = None | |
| config.start_interact_callbacks.append(voice_interact) | |
| config.fast_skipping_callbacks.append(voice_interact) | |
| config.nointeract_callbacks.append(voice_interact) | |
| config.say_sustain_callbacks.append(voice_sustain) | |
| config.afm_voice_delay = .5 | |
| def voice_afm_callback(): | |
| if renpy.sound.is_playing(channel="voice"): | |
| _voice.last_playing = renpy.time.time() | |
| if _preferences.wait_voice: | |
| return renpy.time.time() > (_voice.last_playing + config.afm_voice_delay) | |
| else: | |
| return True | |
| config.afm_callback = voice_afm_callback | |
| def voice_tag_callback(voice_tag): | |
| if _voice.tag is None: | |
| _voice.tag = voice_tag | |
| config.voice_tag_callback = voice_tag_callback | |
| screen _auto_voice: | |
| if _voice.auto_file: | |
| if renpy.loadable(_voice.auto_file): | |
| $ color = "#ffffff" | |
| else: | |
| $ color = "#ffcccc" | |
| frame: | |
| xalign 0.5 | |
| yalign 0.0 | |
| xpadding 5 | |
| ypadding 5 | |
| background "#0004" | |
| text "auto voice: [_voice.auto_file!sq]": | |
| color color | |
| size 12 | |
| python early hide: | |
| def parse_voice(l): | |
| fn = l.simple_expression() | |
| if fn is None: | |
| renpy.error('expected simple expression (string)') | |
| if not l.eol(): | |
| renpy.error('expected end of line') | |
| return fn | |
| def execute_voice(fn): | |
| fn = _audio_eval(fn) | |
| voice(fn) | |
| def predict_voice(fn): | |
| if renpy.emscripten or os.environ.get('RENPY_SIMULATE_DOWNLOAD', False): | |
| fn = config.voice_filename_format.format(filename=_audio_eval(fn)) | |
| try: | |
| with renpy.loader.load(fn) as f: | |
| pass | |
| except renpy.webloader.DownloadNeeded as exception: | |
| renpy.webloader.enqueue(exception.relpath, 'voice', None) | |
| return [ ] | |
| def lint_voice(fn): | |
| _voice.seen_in_lint = True | |
| fn = _try_eval(fn, 'voice filename') | |
| if not isinstance(fn, basestring): | |
| return | |
| try: | |
| fn = config.voice_filename_format.format(filename=fn) | |
| except Exception: | |
| return | |
| if not renpy.music.playable(fn, 'voice'): | |
| renpy.error('voice file %r is not playable' % fn) | |
| renpy.statements.register('voice', | |
| parse=parse_voice, | |
| execute=execute_voice, | |
| predict=predict_voice, | |
| lint=lint_voice, | |
| translatable=True) | |
| def parse_voice_sustain(l): | |
| if not l.eol(): | |
| renpy.error('expected end of line') | |
| return None | |
| def execute_voice_sustain(parsed): | |
| voice_sustain() | |
| renpy.statements.register('voice sustain', | |
| parse=parse_voice_sustain, | |
| execute=execute_voice_sustain, | |
| translatable=True) |