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