# Copyright (C) 2021 GanstaKingofSA (Hanaka) # 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. # Pardon any mess within this PY file. Finally PEP8'd it. import random import re import os import json import renpy import pygame_sdl2 from tinytag import TinyTag from renpy.text.text import Text from renpy.display.im import image import renpy.audio.music as music import renpy.display.behavior as displayBehavior # Creation of Music Room and Code Setup version = 1.6 music.register_channel("music_room", mixer="music_room_mixer", loop=False) if renpy.windows: gamedir = renpy.config.gamedir.replace("\\", "/") elif renpy.android: try: os.mkdir(os.path.join(os.environ["ANDROID_PUBLIC"], "game")) except: pass gamedir = os.path.join(os.environ["ANDROID_PUBLIC"], "game") else: gamedir = renpy.config.gamedir # Lists for holding media types autoDefineList = [] manualDefineList = [] soundtracks = [] file_types = ('.mp3', '.ogg', '.opus', '.wav') # Stores soundtrack in progress game_soundtrack = False # Stores positions of track/volume/default priority time_position = 0.0 time_duration = 3.0 old_volume = 0.0 priorityScan = 2 scale = 1.0 # Stores paused track/player controls game_soundtrack_pause = False prevTrack = False randomSong = False loopSong = False organizeAZ = False organizePriority = True pausedstate = False random.seed() class soundtrack: ''' Class responsible to define songs to the music player. ''' def __init__(self, name="", path="", priority=2, author="", byteTime=False, description="", cover_art=False, unlocked=True): self.name = name self.path = path self.priority = priority self.author = author self.byteTime = byteTime self.description = description if not cover_art: self.cover_art = "images/music_room/nocover.png" else: self.cover_art = cover_art self.unlocked = unlocked @renpy.exports.pure class AdjustableAudioPositionValue(renpy.ui.BarValue): ''' Class that replicates a music progress bar in Ren'Py. ''' def __init__(self, channel='music_room', update_interval=0.0): self.channel = channel self.update_interval = update_interval self.adjustment = None self._hovered = False def get_pos_duration(self): if not music.is_playing(self.channel): pos = time_position else: pos = music.get_pos(self.channel) or 0.0 duration = time_duration return pos, duration def get_song_options_status(self): return loopSong, randomSong def get_adjustment(self): pos, duration = self.get_pos_duration() self.adjustment = renpy.ui.adjustment(value=pos, range=duration, changed=self.set_pos, adjustable=True) return self.adjustment def hovered(self): self._hovered = True def unhovered(self): self._hovered = False def set_pos(self, value): loopThis = self.get_song_options_status() if (self._hovered and pygame_sdl2.mouse.get_pressed()[0]): music.play("".format(value) + game_soundtrack.path, self.channel) if loopThis: music.queue(game_soundtrack.path, self.channel, loop=True) def periodic(self, st): pos, duration = self.get_pos_duration() loopThis, doRandom = self.get_song_options_status() if pos and pos <= duration: self.adjustment.set_range(duration) self.adjustment.change(pos) if pos > duration - 0.20: if loopThis: music.play(game_soundtrack.path, self.channel, loop=True) elif doRandom: random_song() else: next_track() return self.update_interval if renpy.config.screen_width != 1280: scale = renpy.config.screen_width / 1280.0 else: scale = 1.0 def music_pos(style_name, st, at): ''' Returns the track position to Ren'Py. ''' global time_position if music.get_pos(channel='music_room') is not None: time_position = music.get_pos(channel='music_room') readableTime = convert_time(time_position) d = Text(readableTime, style=style_name) return d, 0.20 def music_dur(style_name, st, at): ''' Returns the track duration to Ren'Py. ''' global time_duration if game_soundtrack.byteTime: time_duration = game_soundtrack.byteTime else: time_duration = music.get_duration( channel='music_room') or time_duration readableDuration = convert_time(time_duration) d = Text(readableDuration, style=style_name) return d, 0.20 def dynamic_title_text(style_name, st, at): ''' Returns a resized song title text to Ren'Py. ''' title = len(game_soundtrack.name) if title <= 21: songNameSize = int(37 * scale) elif title <= 28: songNameSize = int(29 * scale) else: songNameSize = int(23 * scale) d = Text(game_soundtrack.name, style=style_name, substitute=False, size=songNameSize) return d, 0.20 def dynamic_author_text(style_name, st, at): ''' Returns a resized song artist text to Ren'Py. ''' author = len(game_soundtrack.author) if author <= 32: authorNameSize = int(25 * scale) elif author <= 48: authorNameSize = int(23 * scale) else: authorNameSize = int(21 * scale) d = Text(game_soundtrack.author, style=style_name, substitute=False, size=authorNameSize) return d, 0.20 def refresh_cover_data(st, at): ''' Returns the song cover art to Ren'Py. ''' d = image(game_soundtrack.cover_art) return d, 0.20 def dynamic_description_text(style_name, st, at): ''' Returns a resized song album/comment to Ren'Py. ''' desc = len(game_soundtrack.description) if desc <= 32: descSize = int(25 * scale) elif desc <= 48: descSize = int(23 * scale) else: descSize = int(21 * scale) d = Text(game_soundtrack.description, style=style_name, substitute=False, size=descSize) return d, 0.20 def auto_play_pause_button(st, at): ''' Returns either a play/pause button to Ren'Py based off song play status. ''' if music.is_playing(channel='music_room'): if pausedstate: d = renpy.display.behavior.ImageButton("images/music_room/pause.png") else: d = renpy.display.behavior.ImageButton("images/music_room/pause.png", action=current_music_pause) else: d = displayBehavior.ImageButton("images/music_room/play.png", action=current_music_play) return d, 0.20 def rpa_mapping_detection(style_name, st, at): ''' Returns a warning message to the player if it can't find the RPA cache JSON file in the game folder. ''' try: renpy.exports.file("RPASongMetadata.json") return Text("", size=23), 0.0 except: return Text("{b}Warning:{/b} The RPA metadata file hasn't been generated. Songs in the {i}track{/i} folder that are archived into a RPA won't work without it. Set {i}config.developer{/i} to {i}True{/i} in order to generate this file.", style=style_name, size=20), 0.0 def convert_time(x): ''' Converts track position and duration to human-readable time. ''' hour = "" if int (x / 3600) > 0: hour = str(int(x / 3600)) if hour != "": if int((x % 3600) / 60) < 10: minute = ":0" + str(int((x % 3600) / 60)) else: minute = ":" + str(int((x % 3600) / 60)) else: minute = "" + str(int(x / 60)) if int(x % 60) < 10: second = ":0" + str(int(x % 60)) else: second = ":" + str(int(x % 60)) return hour + minute + second def current_music_pause(): ''' Pauses the current song playing. ''' global game_soundtrack_pause, pausedstate pausedstate = True if not music.is_playing(channel='music_room'): return else: soundtrack_position = music.get_pos(channel = 'music_room') if soundtrack_position is not None: game_soundtrack_pause = ("" + game_soundtrack.path) music.stop(channel='music_room',fadeout=0) def current_music_play(): ''' Plays either the paused state of the current song or a new song to the player. ''' global pausedstate pausedstate = False if not game_soundtrack_pause: music.play(game_soundtrack.path, channel = 'music_room', fadein=0) else: music.play(game_soundtrack_pause, channel = 'music_room', fadein=0) def current_music_forward(): ''' Fast-forwards the song by 5 seconds or advances to the next song. ''' global game_soundtrack_pause if music.get_pos(channel = 'music_room') is None: soundtrack_position = time_position + 5 else: soundtrack_position = music.get_pos(channel = 'music_room') + 5 if soundtrack_position >= time_duration: game_soundtrack_pause = False if randomSong: random_song() else: next_track() else: game_soundtrack_pause = ("" + game_soundtrack.path) music.play(game_soundtrack_pause, channel = 'music_room') def current_music_backward(): ''' Rewinds the song by 5 seconds or advances to the next song behind it. ''' global game_soundtrack_pause if music.get_pos(channel = 'music_room') is None: soundtrack_position = time_position - 5 else: soundtrack_position = music.get_pos(channel = 'music_room') - 5 if soundtrack_position <= 0.0: game_soundtrack_pause = False next_track(True) else: game_soundtrack_pause = ("" + game_soundtrack.path) music.play(game_soundtrack_pause, channel = 'music_room') def next_track(back=False): ''' Advances to the next song ahead or behind to the player or the start/end. ''' global game_soundtrack for index, item in enumerate(soundtracks): if (game_soundtrack.description == item.description and game_soundtrack.name == item.name): try: if back: game_soundtrack = soundtracks[index-1] else: game_soundtrack = soundtracks[index+1] except: if back: game_soundtrack = soundtracks[-1] else: game_soundtrack = soundtracks[0] break if game_soundtrack != False: music.play(game_soundtrack.path, channel='music_room', loop=loopSong) def random_song(): ''' Advances to the next song with pure randomness. ''' global game_soundtrack unique = 1 if soundtracks[-1].path == game_soundtrack.path: pass else: while unique != 0: a = random.randrange(0, len(soundtracks)-1) if game_soundtrack != soundtracks[a]: unique = 0 game_soundtrack = soundtracks[a] if game_soundtrack != False: music.play(game_soundtrack.path, channel='music_room', loop=loopSong) def mute_player(): ''' Mutes the music player. ''' global old_volume if renpy.game.preferences.get_volume("music_room_mixer") != 0.0: old_volume = renpy.game.preferences.get_volume("music_room_mixer") renpy.game.preferences.set_volume("music_room_mixer", 0.0) else: if old_volume == 0.0: renpy.game.preferences.set_volume("music_room_mixer", 0.5) else: renpy.game.preferences.set_volume("music_room_mixer", old_volume) def refresh_list(): ''' Refreshes the song list. ''' scan_song() if renpy.config.developer or renpy.config.developer == "auto": rpa_mapping() resort() def resort(): ''' Adds songs to the song list and resorts them by priority or A-Z. ''' global soundtracks soundtracks = [] for obj in autoDefineList: if obj.unlocked: soundtracks.append(obj) for obj in manualDefineList: if obj.unlocked: soundtracks.append(obj) if organizeAZ: soundtracks = sorted(soundtracks, key=lambda soundtracks: soundtracks.name) if organizePriority: soundtracks = sorted(soundtracks, key=lambda soundtracks: soundtracks.priority) def get_info(path, tags): ''' Gets the info of the tracks in the track info for defining. ''' sec = tags.duration try: image_data = tags.get_image() with open(os.path.join(gamedir, "python-packages/binaries.txt"), "rb") as a: lines = a.readlines() jpgbytes = bytes("\\xff\\xd8\\xff") utfbytes = bytes("o\\x00v\\x00e\\x00r\\x00\\x00\\x00\\x89PNG\\r\\n") jpgmatch = re.search(jpgbytes, image_data) utfmatch = re.search(utfbytes, image_data) if jpgmatch: cover_formats=".jpg" else: cover_formats=".png" if utfmatch: # addresses itunes cover descriptor fixes image_data = re.sub(utfbytes, lines[2], image_data) coverAlbum = re.sub(r"\[|\]|/|:|\?",'', tags.album) with open(os.path.join(gamedir, 'track/covers', coverAlbum + cover_formats), 'wb') as f: f.write(image_data) art = coverAlbum + cover_formats return tags.title, tags.artist, sec, art, tags.album, tags.comment except TypeError: return tags.title, tags.artist, sec, None, tags.album, tags.comment def scan_song(): ''' Scans the track folder for songs and defines them to the player. ''' global autoDefineList exists = [] for x in autoDefineList[:]: try: renpy.exports.file(x.path) exists.append(x.path) except: autoDefineList.remove(x) for x in os.listdir(gamedir + '/track'): if x.endswith((file_types)) and "track/" + x not in exists: path = "track/" + x tags = TinyTag.get(gamedir + "/" + path, image=True) title, artist, sec, altAlbum, album, comment = get_info(path, tags) def_song(title, artist, path, priorityScan, sec, altAlbum, album, comment, True) def def_song(title, artist, path, priority, sec, altAlbum, album, comment, unlocked=True): ''' Defines the song to the music player list. ''' if title is None: title = str(path.replace("track/", "")).capitalize() if artist is None or artist == "": artist = "Unknown Artist" if altAlbum is None or altAlbum == "": altAlbum = "images/music_room/nocover.png" else: altAlbum = "track/covers/"+altAlbum try: renpy.exports.image_size(altAlbum) except: altAlbum = "images/music_room/nocover.png" if album is None or album == "": description = "Non-Metadata Song" else: if comment is None: description = album else: description = album + '\n' + comment class_name = re.sub(r"-|'| ", "_", title) class_name = soundtrack( name = title, author = artist, path = path, byteTime = sec, priority = priority, description = description, cover_art = altAlbum, unlocked = unlocked ) autoDefineList.append(class_name) def rpa_mapping(): ''' Maps songs in the track folder to a JSON for APK/RPA packing. ''' data = [] try: os.remove(os.path.join(gamedir, "RPASongMetadata.json")) except: pass for y in autoDefineList: data.append ({ "class": re.sub(r"-|'| ", "_", y.name), "title": y.name, "artist": y.author, "path": y.path, "sec": y.byteTime, "altAlbum": y.cover_art, "description": y.description, "unlocked": y.unlocked, }) with open(gamedir + "/RPASongMetadata.json", "a") as f: json.dump(data, f) def rpa_load_mapping(): ''' Loads the JSON mapping and defines it to the player. ''' try: renpy.exports.file("RPASongMetadata.json") except: return with renpy.exports.file("RPASongMetadata.json") as f: data = json.load(f) for p in data: title, artist, path, sec, altAlbum, description, unlocked = (p['title'], p['artist'], p["path"], p["sec"], p["altAlbum"], p["description"], p["unlocked"]) p['class'] = soundtrack( name = title, author = artist, path = path, byteTime = sec, priority = priorityScan, description = description, cover_art = altAlbum, unlocked = unlocked ) autoDefineList.append(p['class']) def get_music_channel_info(): ''' Gets the info of the music channel for exiting purposes. ''' global prevTrack prevTrack = music.get_playing(channel='music') if prevTrack is None: prevTrack = False def check_paused_state(): ''' Checks if the music player is in a paused state for exiting purposes. ''' if not game_soundtrack or pausedstate: return else: current_music_pause() try: os.mkdir(gamedir + "/track") except: pass try: os.mkdir(gamedir + "/track/covers") except: pass for x in os.listdir(gamedir + '/track/covers'): os.remove(gamedir + '/track/covers/' + x) scan_song() if renpy.config.developer or renpy.config.developer == "auto": rpa_mapping() else: rpa_load_mapping() resort()