# Copyright 2004-2019 Tom Rothamel # # 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 contains the code to implement Ren'Py's music room function, # which consists of the ability to set up a playlist (the aforementioned # "music room"), and then a series of actions that let the player # navigate through the music room. init -1500 python: @renpy.pure class __MusicRoomPlay(Action, FieldEquality): """ The action returned by MusicRoom.Play when called with a file. """ identity_fields = [ "mr" ] equality_fields = [ "filename" ] def __init__(self, mr, filename): self.mr = mr self.filename = filename self.selected = self.get_selected() def __call__(self): self.mr.play(self.filename, 0) def get_sensitive(self): return self.mr.is_unlocked(self.filename) def get_selected(self): return renpy.music.get_playing(self.mr.channel) == self.filename def periodic(self, st): if self.selected != self.get_selected(): self.selected = self.get_selected() renpy.restart_interaction() self.mr.periodic(st) return .1 @renpy.pure class __MusicRoomRandomPlay(Action, FieldEquality): """ The action returned by MusicRoom.RandomPlay """ identity_fields = [ "mr" ] def __init__(self, mr): self.mr = mr def __call__(self): playlist = self.mr.unlocked_playlist() if not playlist: return self.mr.shuffled = None self.mr.play(renpy.random.choice(playlist), 0) @renpy.pure class __MusicRoomTogglePlay(Action, FieldEquality): """ The action returned by MusicRoom.TogglePlay """ identity_fields = [ "mr" ] def __init__(self, mr): self.mr = mr def __call__(self): if renpy.music.get_playing(self.mr.channel): return self.mr.stop() self.mr.shuffled = None return self.mr.play() def get_selected(self): return renpy.music.get_playing(self.mr.channel) is not None @renpy.pure class __MusicRoomStop(Action, FieldEquality): """ The action returned by MusicRoom.Stop. """ identity_fields = [ "mr" ] def __init__(self, mr): self.mr = mr self.selected = self.get_selected() def __call__(self): self.mr.stop() def get_selected(self): return renpy.music.get_playing() is None def periodic(self, st): if self.selected != self.get_selected(): self.selected = self.get_selected() renpy.restart_interaction() self.mr.periodic(st) return .1 class MusicRoom(object): """ :doc: music_room class A music room that contains a series of songs that can be unlocked by the user, and actions that can play entries from the list in order. """ loop = False loop_compat = False def __init__(self, channel="music", fadeout=0.0, fadein=0.0, loop=True, single_track=False, shuffle=False, stop_action=None): """ `channel` The channel that this music room will operate on. `fadeout` The number of seconds it takes to fade out the old music when changing tracks. `fadein` The number of seconds it takes to fade in the new music when changing tracks. `loop` Determines if playback will loop or stop when it reaches the end of the playlist. `single_track` If true, only a single track will play. If loop is true, that track will loop. Otherwise, playback will stop when the track finishes. `shuffle` If true, the tracks are shuffled, and played in the shuffled order. If false, the tracks are played in the order they're added to the MusicRoom. `stop_action` An action to run when the music has stopped. Single_track and shuffle conflict with each other. Only one should be true at a time. (Actions that set single_track and shuffle enforce this.) """ self.channel = channel self.fadeout = fadeout self.fadein = fadein # A map from track name (or "" for stopped) to the appropriate # action. self.action = { "" : stop_action } # The last track playing, or "" if we've been stopped. self.last_playing = None # The list of strings giving the titles of songs that make up the # playlist. self.playlist = [ ] # A shuffled copy of the playlist. (Created on demand when we # need it.) self.shuffled = None # A set of filenames, so we can quickly check if a valid filename # has been provided. self.filenames = set() # The set of songs that are always unlocked. self.always_unlocked = set() # Should we loop a single track rather than advancing to the next # track? self.loop = loop # Should we play a single track? self.single_track = single_track # Should we shuffle the playlist? if self.single_track: self.shuffle = False else: self.shuffle = shuffle # In older versions, loop would loop a single trak. if self.loop_compat and loop: self.single_track = True # The last shown time for the music room. self.st = -1 def periodic(self, st): if st == self.st: return elif st < self.st: self.last_playing = None self.st = st current_playing = renpy.music.get_playing(self.channel) if current_playing is None: current_playing = "" if self.last_playing != current_playing: action = self.action.get(current_playing, None) renpy.run_action(action) self.last_playing = current_playing def add(self, filename, always_unlocked=False, action=None): """ :doc: music_room method Adds the music file `filename` to this music room. The music room will play unlocked files in the order that they are added to the room. `always_unlocked` If true, the music file will be always unlocked. This allows the file to show up in the music room before it has been played in the game. `action` This is a action or the list of actions. these are called when this file is played. For example, These actions is used to change a screen or background, description by the playing file. """ self.playlist.append(filename) self.filenames.add(filename) if action: self.action[filename] = action if always_unlocked: self.always_unlocked.add(filename) def is_unlocked(self, filename): """ :doc: music_room method Returns true if the filename has been unlocked (or is always unlocked), and false if it is still locked. """ if filename in self.always_unlocked: return True return renpy.seen_audio(filename) def unlocked_playlist(self, filename=None): """ Returns a list of filenames in the playlist that have been unlocked. """ if self.shuffle: if self.shuffled is None or (filename and self.shuffled[0] != filename): import random self.shuffled = list(self.playlist) random.shuffle(self.shuffled) if filename in self.shuffled: self.shuffled.remove(filename) self.shuffled.insert(0, filename) playlist = self.shuffled else: self.shuffled = None playlist = self.playlist return [ i for i in playlist if self.is_unlocked(i) ] def play(self, filename=None, offset=0, queue=False): """ Starts the music room playing. The file we start playing with is selected in two steps. If `filename` is an unlocked file, we start by playing it. Otherwise, we start by playing the currently playing file, and if that doesn't exist or isn't unlocked, we start with the first file. We then apply `offset`. If `offset` is positive, we advance that many files, otherwise we go back that many files. If `queue` is true, the music is queued. Otherwise, it is played immediately. """ playlist = self.unlocked_playlist(filename) if not playlist: return if filename is None: filename = renpy.music.get_playing(channel=self.channel) try: idx = playlist.index(filename) except ValueError: idx = 0 idx = (idx + offset) % len(playlist) if self.single_track: playlist = [ playlist[idx] ] elif self.loop: playlist = playlist[idx:] + playlist[:idx] else: playlist = playlist[idx:] if queue: renpy.music.queue(playlist, channel=self.channel, loop=self.loop) else: renpy.music.play(playlist, channel=self.channel, fadeout=self.fadeout, fadein=self.fadein, loop=self.loop) def queue_if_playing(self): """ If the music is not playing, do nothing. Otherwise, redo the queue such that we have the right tracks queued up. """ filename = renpy.music.get_playing(channel=self.channel) if filename is None: return if self.single_track: self.play(None, offset=0, queue=True) else: self.play(None, offset=1, queue=True) def stop(self): """ Stops the music from playing. """ renpy.music.stop(channel=self.channel, fadeout=self.fadeout) def next(self): """ Plays the next file in the playlist. """ filename = renpy.music.get_playing(channel=self.channel) if filename is None: return self.play(None, 0) else: return self.play(None, 1) def previous(self): """ Plays the previous file in the playlist. """ return self.play(None, -1) def Play(self, filename=None): """ :doc: music_room method This action causes the music room to start playing. If `filename` is given, that file begins playing. Otherwise, the currently playing file starts over (if it's unlocked), or the first file starts playing. If `filename` is given, buttons with this action will be insensitive while `filename` is locked, and will be selected when `filename` is playing. """ if filename is None: return self.play if filename not in self.filenames: raise Exception("{0!r} is not a filename registered with this music room.".format(filename)) return __MusicRoomPlay(self, filename) def RandomPlay(self): """ :doc: music_room method This action causes the music room to start playing a randomly selected unlocked music track. """ return __MusicRoomRandomPlay(self) def TogglePlay(self): """ :doc: music_room method If no music is currently playing, this action starts playing the first unlocked track. Otherwise, stops the currently playing music. This button is selected when any music is playing. """ return __MusicRoomTogglePlay(self) def Stop(self): """ :doc: music_room method This action stops the music. """ return __MusicRoomStop(self) def Next(self): """ :doc: music_room method An action that causes the music room to play the next unlocked file in the playlist. """ return self.next def Previous(self): """ :doc: music_room method An action that causes the music room to play the previous unlocked file in the playlist. """ return self.previous def SetLoop(self, value): """ :doc: music_room method This action sets the value of the loop property. """ return [ SetField(self, "loop", value), self.queue_if_playing ] def SetSingleTrack(self, value): """ :doc: music_room method This action sets the value of the single_track property. """ if value: return [SelectedIf(self.single_track), SetField(self, "single_track", value), SetField(self, "shuffle", False), self.queue_if_playing ] else: return [SelectedIf(not self.single_track), SetField(self, "single_track", value), self.queue_if_playing ] def SetShuffle(self, value): """ :doc: music_room method This action sets the value of the shuffle property. """ if value: return [SelectedIf(self.shuffle), SetField(self, "shuffle", value), SetField(self, "single_track", False), self.queue_if_playing ] else: return [SelectedIf(not self.shuffle), SetField(self, "shuffle", value), self.queue_if_playing ] def ToggleLoop(self): """ :doc: music_room method This action toggles the value of the loop property. """ return [ ToggleField(self, "loop"), self.queue_if_playing ] def ToggleSingleTrack(self): """ :doc: music_room method This action toggles the value of the single_track property. """ return [SelectedIf(self.single_track), ToggleField(self, "single_track"), SetField(self, "shuffle", False), self.queue_if_playing ] def ToggleShuffle(self): """ :doc: music_room method This action toggles the value of the shuffle property. """ return [SelectedIf(self.shuffle), ToggleField(self, "shuffle"), SetField(self, "single_track", False), self.queue_if_playing ]