1# Copyright 2004-2021 Tom Rothamel <pytom@bishoujo.us>
2#
3# Permission is hereby granted, free of charge, to any person
4# obtaining a copy of this software and associated documentation files
5# (the "Software"), to deal in the Software without restriction,
6# including without limitation the rights to use, copy, modify, merge,
7# publish, distribute, sublicense, and/or sell copies of the Software,
8# and to permit persons to whom the Software is furnished to do so,
9# subject to the following conditions:
10#
11# The above copyright notice and this permission notice shall be
12# included in all copies or substantial portions of the Software.
13#
14# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
22# This extra contains a basic implementation of voice support. Right
23# now, voice is given its own toggle, and can either be turned on or
24# turned off. In the future, we'll probably provide some way of
25# toggling it on or off for individual characters.
26#
27# To use it, place a voice "<sndfile>" line before each voiced line of
28# dialogue.
29#
30#     voice "e_1001.ogg"
31#     e "Voice support lets you add the spoken word to your games."
32#
33# Normally, a voice is cancelled at the start of the next
34# interaction. If you want a voice to span interactions, call
35# voice_sustain.
36#
37#     voice "e_1002.ogg"
38#     e "Voice sustain is a technique that allows the same voice file.."
39#
40#     $ voice_sustain()
41#     e "...to play for two lines of dialogue."
42
43init -1500 python:
44
45    _voice = object()
46    _voice.play = None
47    _voice.sustain = False
48    _voice.seen_in_lint = False
49    _voice.tag = None
50    _voice.tlid = None
51    _voice.auto_file = None
52    _voice.info = None
53    _voice.last_playing = 0.0
54
55    # If true, the voice system ignores the interaction.
56    _voice.ignore_interaction = False
57
58    # The voice filename format. This may contain the voice tag
59    config.voice_filename_format = "{filename}"
60
61    # This is formatted with {id} to produce a filename. If the filename
62    # exists, it's played as a voice file.
63    config.auto_voice = None
64
65    # The last sound played on the voice channel. (This is used to replay
66    # it.)
67    _last_voice_play = None
68
69
70    # Call this to specify the voice file that will be played for
71    # the user. This peice only gathers the information so
72    # voice_interact can play the right file.
73    def voice(filename, tag=None):
74        """
75        :doc: voice
76
77        Plays `filename` on the voice channel. The equivalent of the voice
78        statement.
79
80        `filename`
81            The filename to play. This is used with
82            :var:`config.voice_filename_format` to produce the
83            filename that will be played.
84
85        `tag`
86            If this is not None, it should be a string giving a
87            voice tag to be played. If None, this takes its
88            default value from the voice_tag of the Character
89            that causes the next interaction.
90
91            The voice tag is used to specify which character is
92            speaking, to allow a user to mute or unmute the
93            voices of particular characters.
94        """
95
96        if not config.has_voice:
97            return
98
99        fn = config.voice_filename_format.format(filename=filename)
100        _voice.play = fn
101        _voice.tag = tag
102
103
104    # Call this to specify that the currently playing voice file
105    # should be sustained through the current interaction.
106    def voice_sustain(ignored="", **kwargs):
107        """
108        :doc: voice
109
110        The equivalent of the voice sustain statement.
111        """
112
113        if not config.has_voice:
114            return
115
116        _voice.sustain = True
117
118    # Call this to replay the last bit of voice.
119    def voice_replay():
120        """
121        :doc: voice
122
123        Replays the current voice, if possible.
124        """
125
126        if _last_voice_play is not None:
127            renpy.sound.play(_last_voice_play, channel="voice")
128
129    # Returns true if we can replay the voice.
130    def voice_can_replay():
131        """
132        :doc: voice
133
134        Returns true if it's possible to replay the current voice.
135        """
136
137        return _last_voice_play is not None
138
139    @renpy.pure
140    class SetVoiceMute(Action, DictEquality):
141        """
142        :doc: voice_action
143
144        If `mute` is true, mutes voices that are played with the given
145        `voice_tag`. If `mute` is false, unmutes voices that are played
146        with `voice_tag`.
147        """
148
149        def __init__(self, voice_tag, mute):
150            self.voice_tag = voice_tag
151            self.mute = mute
152
153        def get_selected(self):
154            if self.mute:
155                return self.voice_tag in persistent._voice_mute
156            else:
157                return self.voice_tag not in persistent._voice_mute
158
159        def __call__(self):
160            if self.mute:
161                persistent._voice_mute.add(self.voice_tag)
162            else:
163                persistent._voice_mute.discard(self.voice_tag)
164
165            renpy.restart_interaction()
166
167    @renpy.pure
168    def SetCharacterVolume(voice_tag, volume=None):
169        """
170        :doc: voice_action
171
172        This allows the volume of each characters to be adjusted.
173        If `volume` is None, this returns a BarValue that
174        controls the value of `voice_tag`. Otherwise, this set it to `volume`.
175
176        `volume` is a number between 0.0 and 1.0, and is interpreted as a
177        fraction of the mixer volume for `voice` channel.
178        """
179
180        if voice_tag not in persistent._character_volume:
181            persistent._character_volume[voice_tag] = 1.0
182
183        if volume is None:
184            return DictValue(persistent._character_volume, voice_tag, 1.0)
185        else:
186            return SetDict(persistent._character_volume, voice_tag, volume)
187
188    def GetCharacterVolume(voice_tag):
189        """
190        :doc: preference_functions
191
192        This returns the volume associated with voice tag, a number
193        between 0.0 and 1.0, which is interpreted as a fraction of the
194        mixer volume for the `voice` channel.
195        """
196
197        return persistent._character_volume.get(voice_tag, 1.0)
198
199    @renpy.pure
200    class PlayCharacterVoice(Action, FieldEquality):
201        """
202        :doc: voice_action
203
204        This plays `sample` on the voice channel, as if said by a
205        character with `voice_tag`.
206
207        `sample`
208            The full path to a sound file. No voice-related handling
209            of this file is done.
210
211        `selected`
212            If True, buttons using this action will be marked as selected
213            while the sample is playing.
214        """
215
216        equality_fields = [ "voice_tag", "sample", "can_be_selected" ]
217
218        can_be_selected = False
219        selected = False
220
221        def __init__(self, voice_tag, sample, selected=False):
222            self.voice_tag = voice_tag
223            self.sample = sample
224
225            self.can_be_selected = selected
226
227        def __call__(self):
228            if self.voice_tag in persistent._voice_mute:
229                return
230
231            volume = persistent._character_volume.get(self.voice_tag, 1.0)
232            renpy.music.get_channel("voice").set_volume(volume)
233
234            renpy.sound.play(self.sample, channel="voice")
235            renpy.restart_interaction()
236            self.periodic(0)
237
238        def get_selected(self):
239
240            if not self.can_be_selected:
241                return False
242
243            return renpy.sound.get_playing(channel="voice") == self.sample
244
245        def periodic(self, st):
246
247            if not self.can_be_selected:
248                return None
249
250            old_selected = self.selected
251            new_selected = self.get_selected()
252
253            if old_selected != new_selected:
254                renpy.restart_interaction()
255                self.selected = new_selected
256
257            return .1
258
259    @renpy.pure
260    class ToggleVoiceMute(Action, DictEquality):
261        """
262        :doc: voice_action
263
264        Toggles the muting of `voice_tag`. This is selected if
265        the given voice tag is muted, unless `invert` is true,
266        in which case it's selected if the voice is unmuted.
267        """
268
269        def __init__(self, voice_tag, invert=False):
270            self.voice_tag = voice_tag
271            self.invert = invert
272
273
274        def get_selected(self):
275            rv = self.voice_tag in persistent._voice_mute
276
277            if self.invert:
278                return not rv
279            else:
280                return rv
281
282        def __call__(self):
283            if self.voice_tag not in persistent._voice_mute:
284                persistent._voice_mute.add(self.voice_tag)
285            else:
286                persistent._voice_mute.discard(self.voice_tag)
287
288            renpy.restart_interaction()
289
290    @renpy.pure
291    class VoiceReplay(Action, DictEquality):
292        """
293        :doc: voice_action
294
295        Replays the most recently played voice.
296        """
297
298        def __call__(self):
299            voice_replay()
300
301        def get_sensitive(self):
302            return voice_can_replay()
303
304
305    class VoiceInfo(_object):
306        """
307        An object returned by VoiceInfo and get_voice_info().
308        """
309
310        def __init__(self):
311
312            self.filename = _voice.play
313            self.auto_filename = None
314            self.tlid = None
315            self.sustain = _voice.sustain
316            self.tag = _voice.tag
317
318            if not self.filename and config.auto_voice:
319
320                for tlid in [
321                    renpy.game.context().translate_identifier,
322                    renpy.game.context().alternate_translate_identifier,
323                    renpy.game.context().deferred_translate_identifier,
324                    ]:
325
326                    if tlid is None:
327                        continue
328
329                    if isinstance(config.auto_voice, (str, unicode)):
330                        fn = config.auto_voice.format(id=tlid)
331                    else:
332                        fn = config.auto_voice(tlid)
333
334                    self.auto_filename = fn
335
336                    if fn and renpy.loadable(fn):
337
338                        if _voice.tlid == tlid:
339                            self.sustain = True
340                        else:
341                            self.filename = fn
342
343                        break
344
345            self.tlid = renpy.game.context().translate_identifier or renpy.game.context().deferred_translate_identifier
346
347            if self.filename:
348                self.sustain = False
349            elif self.sustain and (self.sustain != "preference"):
350                self.filename = _last_voice_play
351
352
353    def _get_voice_info():
354        """
355        :doc: voice
356
357        Returns information about the voice being played by the current
358        say statement. This function may only be called while a say statement
359        is executing.
360
361        The object returned has the following fields:
362
363        .. attribute:: VoiceInfo.filename
364
365            The filename of the voice to be played, or None if no files
366            should be played.
367
368        .. attribute:: VoiceInfo.auto_filename
369
370            The filename that Ren'Py looked in for automatic-voicing
371            purposes, or None if one could not be found.
372
373        .. attribute:: VoiceInfo.tag
374
375            The voice_tag parameter supplied to the speaking Character.
376
377        .. attribute:: VoiceInfo.sustain
378
379            False if the file was played as part of this interaction. True if
380            it was sustained from a previous interaction.
381
382        """
383
384        vi = VoiceInfo()
385
386        if _voice.info is None:
387            return vi
388        elif _voice.info.tlid == vi.tlid:
389            return _voice.info
390        else:
391            return vi
392
393    def _voice_history_callback(h):
394        h.voice = _get_voice_info()
395
396    config.history_callbacks.append(_voice_history_callback)
397
398
399init -1500 python hide:
400
401    # basics: True if the game will have voice.
402    config.has_voice = True
403
404    # The set of voice tags that are currently muted.
405    if persistent._voice_mute is None:
406        persistent._voice_mute = set()
407
408    # The dictionary of the volume of each voice tags.
409    if persistent._character_volume is None:
410        persistent._character_volume = dict()
411
412    # This is called on each interaction, to ensure that the
413    # appropriate voice file is played for the user.
414    def voice_interact():
415
416        if not config.has_voice:
417            return
418
419        if _voice.ignore_interaction:
420            return
421
422        mode = renpy.get_mode()
423
424        if (mode is None) or (mode == "with"):
425            return
426
427        if getattr(renpy.context(), "_menu", False):
428            renpy.sound.stop(channel="voice")
429            return
430
431        if _preferences.voice_sustain and not _voice.sustain:
432            _voice.sustain = "preference"
433
434        if _voice.play:
435            _voice.sustain = False
436
437        vi = VoiceInfo()
438
439        if not _voice.sustain:
440            _voice.info = vi
441
442        if not vi.sustain:
443            _voice.play = vi.filename
444        else:
445            _voice.play = None
446
447        renpy.game.context().deferred_translate_identifier = None
448
449        _voice.auto_file = vi.auto_filename
450        _voice.sustain = vi.sustain
451        _voice.tlid = vi.tlid
452
453        volume = persistent._character_volume.get(_voice.tag, 1.0)
454
455        if (not volume) or (_voice.tag in persistent._voice_mute):
456            renpy.sound.stop(channel="voice")
457            store._last_voice_play = _voice.play
458
459        elif _voice.play:
460            if not config.skipping:
461                renpy.music.get_channel("voice").set_volume(volume)
462                renpy.sound.play(_voice.play, channel="voice")
463
464            store._last_voice_play = _voice.play
465
466        elif not _voice.sustain:
467            renpy.sound.stop(channel="voice")
468
469            if not getattr(renpy.context(), "_menu", False):
470                store._last_voice_play = None
471
472        _voice.play = None
473        _voice.sustain = False
474        _voice.tag = None
475
476    config.start_interact_callbacks.append(voice_interact)
477    config.fast_skipping_callbacks.append(voice_interact)
478    config.say_sustain_callbacks.append(voice_sustain)
479    config.afm_voice_delay = .5
480
481    def voice_afm_callback():
482
483        if renpy.sound.is_playing(channel="voice"):
484            _voice.last_playing = renpy.time.time()
485
486        if _preferences.wait_voice:
487            return renpy.time.time() > (_voice.last_playing + config.afm_voice_delay)
488        else:
489            return True
490
491    config.afm_callback = voice_afm_callback
492
493    def voice_tag_callback(voice_tag):
494
495        if _voice.tag is None:
496            _voice.tag = voice_tag
497
498    config.voice_tag_callback = voice_tag_callback
499
500
501screen _auto_voice:
502
503    if _voice.auto_file:
504
505        if renpy.loadable(_voice.auto_file):
506            $ color = "#ffffff"
507        else:
508            $ color = "#ffcccc"
509
510        frame:
511            xalign 0.5
512            yalign 0.0
513            xpadding 5
514            ypadding 5
515            background "#0004"
516
517            text "auto voice: [_voice.auto_file!sq]":
518                color color
519                size 12
520
521python early hide:
522
523    def parse_voice(l):
524        fn = l.simple_expression()
525        if fn is None:
526            renpy.error('expected simple expression (string)')
527
528        if not l.eol():
529            renpy.error('expected end of line')
530
531        return fn
532
533    def execute_voice(fn):
534        fn = _audio_eval(fn)
535        voice(fn)
536
537    def predict_voice(fn):
538        if renpy.emscripten or os.environ.get('RENPY_SIMULATE_DOWNLOAD', False):
539            fn = config.voice_filename_format.format(filename=_audio_eval(fn))
540            try:
541                with renpy.loader.load(fn) as f:
542                    pass
543            except renpy.webloader.DownloadNeeded as exception:
544                renpy.webloader.enqueue(exception.relpath, 'voice', None)
545        return [ ]
546
547    def lint_voice(fn):
548        _voice.seen_in_lint = True
549
550        fn = _try_eval(fn, 'voice filename')
551        if not isinstance(fn, basestring):
552            return
553
554        try:
555            fn = config.voice_filename_format.format(filename=fn)
556        except:
557            return
558
559        if not renpy.music.playable(fn, 'voice'):
560            renpy.error('voice file %r is not playable' % fn)
561
562    renpy.statements.register('voice',
563                              parse=parse_voice,
564                              execute=execute_voice,
565                              predict=predict_voice,
566                              lint=lint_voice,
567                              translatable=True)
568
569    def parse_voice_sustain(l):
570        if not l.eol():
571            renpy.error('expected end of line')
572
573        return None
574
575    def execute_voice_sustain(parsed):
576        voice_sustain()
577
578    renpy.statements.register('voice sustain',
579                              parse=parse_voice_sustain,
580                              execute=execute_voice_sustain,
581                              translatable=True)
582