diff --git a/game/RPASongMetadata.json b/game/RPASongMetadata.json new file mode 100644 index 0000000..07647ba --- /dev/null +++ b/game/RPASongMetadata.json @@ -0,0 +1 @@ +[{"class": "Amberlight_brilliance___demo.ogg", "title": "Amberlight brilliance - demo.ogg", "artist": "Unknown Artist", "path": "track/Amberlight Brilliance - Demo.ogg", "sec": 115.77469387755102, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "Amberlight_brilliance___live.ogg", "title": "Amberlight brilliance - live.ogg", "artist": "Unknown Artist", "path": "track/Amberlight Brilliance - Live.ogg", "sec": 54.276643990929706, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "dinolove_piano", "title": "dinolove-piano", "artist": "Cilantro", "path": "track/Amberlight Brilliance - Piano.ogg", "sec": 36.15, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "Amberlight_brilliance_d_ending.ogg", "title": "Amberlight brilliance d ending.ogg", "artist": "Unknown Artist", "path": "track/Amberlight Brilliance D ending.ogg", "sec": 112.0, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "Amberlight_brilliance.ogg", "title": "Amberlight brilliance.ogg", "artist": "Unknown Artist", "path": "track/Amberlight Brilliance.ogg", "sec": 207.36274376417234, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "mellow_piano", "title": "mellow piano", "artist": "Unknown Artist", "path": "track/Appreciating her Company.ogg", "sec": 132.17390022675738, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "appreciating_the_atomosphere_cleaned", "title": "appreciating the atomosphere cleaned", "artist": "Unknown Artist", "path": "track/Appreciating the Atmosphere.ogg", "sec": 135.529410430839, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "mellow_rock", "title": "mellow rock", "artist": "Unknown Artist", "path": "track/Appreciating the Moment.ogg", "sec": 144.0, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "appreciating_the_scenery", "title": "appreciating the scenery", "artist": "Unknown Artist", "path": "track/appreciating_the_scenery.ogg", "sec": 61.09090702947846, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "not_as_short", "title": "not as short", "artist": "Unknown Artist", "path": "track/bad_music.ogg", "sec": 5.45453514739229, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "ballad_of_the_boot", "title": "ballad of the boot", "artist": "Unknown Artist", "path": "track/ballad_of_the_boot.ogg", "sec": 45.17646258503402, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "surfing", "title": "surfing", "artist": "Unknown Artist", "path": "track/Bayside Bumming it.ogg", "sec": 116.84208616780046, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "bayside_revamp", "title": "bayside revamp", "artist": "Unknown Artist", "path": "track/bayside_revamp.ogg", "sec": 44.210521541950115, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "beach_chill_out", "title": "beach chill out", "artist": "Unknown Artist", "path": "track/Beach Chill Out.ogg", "sec": 90.35292517006803, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "stella_rework_2", "title": "stella rework 2", "artist": "Unknown Artist", "path": "track/Dino Destiny Reader.ogg", "sec": 56.72725623582767, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "dinolove_piano", "title": "dinolove-piano", "artist": "Cilantro", "path": "track/dinolove.ogg", "sec": 36.15, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "latin_funk_max", "title": "latin funk max", "artist": "Unknown Artist", "path": "track/Dragging on and on....ogg", "sec": 128.0, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "fighter", "title": "fighter", "artist": "Unknown Artist", "path": "track/fighter.ogg", "sec": 50.823514739229026, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "american_punk", "title": "american punk", "artist": "Unknown Artist", "path": "track/Fuck You I Like These Weirdos.ogg", "sec": 45.714285714285715, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "pure_funk", "title": "pure funk", "artist": "Unknown Artist", "path": "track/Fuck You I Like This Chick.ogg", "sec": 58.542585034013605, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "punky_funky", "title": "punky funky", "artist": "Unknown Artist", "path": "track/Fuck You I Like To Ignore Problems.ogg", "sec": 93.17646258503402, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "punker", "title": "punker", "artist": "Unknown Artist", "path": "track/Fuck You I Like To Shitpost.ogg", "sec": 73.46938775510205, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "Good_faith_leadup_drums_synth.ogg", "title": "Good faith leadup drums synth.ogg", "artist": "Unknown Artist", "path": "track/good faith leadup drums synth.ogg", "sec": 123.42857142857143, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "Good_faith_leadup_piano.ogg", "title": "Good faith leadup piano.ogg", "artist": "Unknown Artist", "path": "track/good faith leadup piano.ogg", "sec": 123.42857142857143, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "Good_faith.ogg", "title": "Good faith.ogg", "artist": "Unknown Artist", "path": "track/good faith.ogg", "sec": 93.8587074829932, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "intercept", "title": "intercept", "artist": "Unknown Artist", "path": "track/intercept.ogg", "sec": 80.0, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "its_footloose_now", "title": "its footloose now", "artist": "Unknown Artist", "path": "track/its_footloose_now.ogg", "sec": 93.20390022675737, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "fang", "title": "fang", "artist": "Unknown Artist", "path": "track/its_the_original_you_didnt_ask_for.ogg", "sec": 78.54544217687075, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "i_have_no_clue_what_im_doing", "title": "i have no clue what im doing", "artist": "Unknown Artist", "path": "track/i_have_no_clue_what_im_doing.ogg", "sec": 85.33331065759637, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "literal_club_music", "title": "literal club music", "artist": "Unknown Artist", "path": "track/Many Such Cases of Being So True.ogg", "sec": 64.0, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "middle_of_nowhere", "title": "middle of nowhere", "artist": "Unknown Artist", "path": "track/middle_of_nowhere.ogg", "sec": 65.64102040816327, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "disco_1", "title": "disco 1", "artist": "Unknown Artist", "path": "track/Prized Bowling Ball.ogg", "sec": 94.42621315192744, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "short_and_sweet", "title": "short and sweet", "artist": "Unknown Artist", "path": "track/Prized Bowling Shoes.ogg", "sec": 86.55637188208617, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "prom_without_chatter_early_2", "title": "prom_without_chatter early 2", "artist": "Unknown Artist", "path": "track/prom1.ogg", "sec": 16.0, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "prom_without_chatter", "title": "prom_without_chatter", "artist": "Unknown Artist", "path": "track/prom2.ogg", "sec": 16.0, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "protestra_punk", "title": "protestra punk", "artist": "Unknown Artist", "path": "track/protestra_punk.ogg", "sec": 69.92124716553288, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "punk_revamp", "title": "punk revamp", "artist": "Unknown Artist", "path": "track/punk_revamp.ogg", "sec": 59.294104308390025, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "rosa_s_theme", "title": "rosa's theme", "artist": "Unknown Artist", "path": "track/Ramo de Rosas.ogg", "sec": 57.43589569160998, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "Sad!.ogg", "title": "Sad!.ogg", "artist": "Unknown Artist", "path": "track/Sad!.ogg", "sec": 47.185918367346936, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "sadist", "title": "sadist", "artist": "Unknown Artist", "path": "track/sadist.ogg", "sec": 53.333333333333336, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "how_we_talk_cluttered", "title": "how we talk cluttered", "artist": "Unknown Artist", "path": "track/sage goes in all fields.ogg", "sec": 124.8, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "she_fucks_human_men", "title": "she fucks human men", "artist": "Unknown Artist", "path": "track/she_fucks_human_men.ogg", "sec": 123.87095238095237, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "calm_casual_theme", "title": "calm casual theme", "artist": "Unknown Artist", "path": "track/Skinrow Soul.ogg", "sec": 73.21931972789116, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "summertime_synth", "title": "summertime synth", "artist": "Unknown Artist", "path": "track/Summertime Synth.ogg", "sec": 92.30768707482993, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "synthy_rock", "title": "synthy rock", "artist": "Unknown Artist", "path": "track/That Almost Sounded Good.ogg", "sec": 30.857142857142858, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "fang_extended", "title": "fang extended", "artist": "Unknown Artist", "path": "track/That Monochromatic Weirdo.ogg", "sec": 52.96551020408163, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "extended_psyche", "title": "extended psyche", "artist": "Unknown Artist", "path": "track/The (audiophile edition).ogg", "sec": 88.27480725623583, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "speed_metal", "title": "speed metal", "artist": "Unknown Artist", "path": "track/The Hitler Song With The Really Long Name.ogg", "sec": 195.348820861678, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "anon_room_2_slowed_2", "title": "anon room 2 slowed 2", "artist": "Unknown Artist", "path": "track/The Hunt for more (You)s.ogg", "sec": 35.99995464852608, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "grungy_grunger", "title": "grungy grunger", "artist": "Unknown Artist", "path": "track/The Top of the Social Ladder.ogg", "sec": 50.086938775510205, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "extended_psyche", "title": "extended psyche", "artist": "Unknown Artist", "path": "track/The.ogg", "sec": 88.27480725623583, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "this_hurts", "title": "this hurts", "artist": "Unknown Artist", "path": "track/this_hurts.ogg", "sec": 26.363968253968252, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "ska", "title": "ska", "artist": "Unknown Artist", "path": "track/Those Other Two Weirdos.ogg", "sec": 42.66664399092971, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "to_swager", "title": "to swager", "artist": "Unknown Artist", "path": "track/to_swagger.ogg", "sec": 100.0, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "southie", "title": "southie", "artist": "Unknown Artist", "path": "track/Tracy was fired from her real job.ogg", "sec": 85.89471655328798, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "flutier", "title": "flutier", "artist": "Unknown Artist", "path": "track/Venetian Values.ogg", "sec": 65.28, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "to_spear_a_conversation", "title": "to spear a conversation", "artist": "Unknown Artist", "path": "track/we just turned on the microphone in our programmers_ house.ogg", "sec": 57.14283446712018, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "west_coast_kicking", "title": "west coast kicking", "artist": "Unknown Artist", "path": "track/west_coast_kicking.ogg", "sec": 69.12, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "drama", "title": "drama", "artist": "Unknown Artist", "path": "track/you can_t sage here.ogg", "sec": 38.4, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}, {"class": "fight", "title": "fight", "artist": "Unknown Artist", "path": "track/you need gopher chucks.ogg", "sec": 36.92306122448979, "altAlbum": "images/music_room/nocover.png", "description": "Non-Metadata Song", "unlocked": true}] \ No newline at end of file diff --git a/game/images/music_room/A-Z.png b/game/images/music_room/A-Z.png new file mode 100644 index 0000000..09c7684 Binary files /dev/null and b/game/images/music_room/A-Z.png differ diff --git a/game/images/music_room/A-ZHover.png b/game/images/music_room/A-ZHover.png new file mode 100644 index 0000000..ea4e584 Binary files /dev/null and b/game/images/music_room/A-ZHover.png differ diff --git a/game/images/music_room/A-ZOn.png b/game/images/music_room/A-ZOn.png new file mode 100644 index 0000000..21af2cf Binary files /dev/null and b/game/images/music_room/A-ZOn.png differ diff --git a/game/images/music_room/backward.png b/game/images/music_room/backward.png new file mode 100644 index 0000000..0798f8b Binary files /dev/null and b/game/images/music_room/backward.png differ diff --git a/game/images/music_room/backwardHover.png b/game/images/music_room/backwardHover.png new file mode 100644 index 0000000..a0f3508 Binary files /dev/null and b/game/images/music_room/backwardHover.png differ diff --git a/game/images/music_room/forward.png b/game/images/music_room/forward.png new file mode 100644 index 0000000..070fcb5 Binary files /dev/null and b/game/images/music_room/forward.png differ diff --git a/game/images/music_room/forwardHover.png b/game/images/music_room/forwardHover.png new file mode 100644 index 0000000..674ca1f Binary files /dev/null and b/game/images/music_room/forwardHover.png differ diff --git a/game/images/music_room/nocover.png b/game/images/music_room/nocover.png new file mode 100644 index 0000000..de6f942 Binary files /dev/null and b/game/images/music_room/nocover.png differ diff --git a/game/images/music_room/pause.png b/game/images/music_room/pause.png new file mode 100644 index 0000000..1630f4f Binary files /dev/null and b/game/images/music_room/pause.png differ diff --git a/game/images/music_room/play.png b/game/images/music_room/play.png new file mode 100644 index 0000000..fa2bb89 Binary files /dev/null and b/game/images/music_room/play.png differ diff --git a/game/images/music_room/priority.png b/game/images/music_room/priority.png new file mode 100644 index 0000000..060c3be Binary files /dev/null and b/game/images/music_room/priority.png differ diff --git a/game/images/music_room/priorityHover.png b/game/images/music_room/priorityHover.png new file mode 100644 index 0000000..ea47745 Binary files /dev/null and b/game/images/music_room/priorityHover.png differ diff --git a/game/images/music_room/priorityOn.png b/game/images/music_room/priorityOn.png new file mode 100644 index 0000000..c90e4ff Binary files /dev/null and b/game/images/music_room/priorityOn.png differ diff --git a/game/images/music_room/refreshHover.png b/game/images/music_room/refreshHover.png new file mode 100644 index 0000000..4220565 Binary files /dev/null and b/game/images/music_room/refreshHover.png differ diff --git a/game/images/music_room/refreshList.png b/game/images/music_room/refreshList.png new file mode 100644 index 0000000..2498074 Binary files /dev/null and b/game/images/music_room/refreshList.png differ diff --git a/game/images/music_room/replay.png b/game/images/music_room/replay.png new file mode 100644 index 0000000..239a528 Binary files /dev/null and b/game/images/music_room/replay.png differ diff --git a/game/images/music_room/replayHover.png b/game/images/music_room/replayHover.png new file mode 100644 index 0000000..5bc1884 Binary files /dev/null and b/game/images/music_room/replayHover.png differ diff --git a/game/images/music_room/replayOn.png b/game/images/music_room/replayOn.png new file mode 100644 index 0000000..844c93a Binary files /dev/null and b/game/images/music_room/replayOn.png differ diff --git a/game/images/music_room/shuffle.png b/game/images/music_room/shuffle.png new file mode 100644 index 0000000..b494a4d Binary files /dev/null and b/game/images/music_room/shuffle.png differ diff --git a/game/images/music_room/shuffleHover.png b/game/images/music_room/shuffleHover.png new file mode 100644 index 0000000..62cc586 Binary files /dev/null and b/game/images/music_room/shuffleHover.png differ diff --git a/game/images/music_room/shuffleOn.png b/game/images/music_room/shuffleOn.png new file mode 100644 index 0000000..2264c12 Binary files /dev/null and b/game/images/music_room/shuffleOn.png differ diff --git a/game/images/music_room/volume.png b/game/images/music_room/volume.png new file mode 100644 index 0000000..778c15f Binary files /dev/null and b/game/images/music_room/volume.png differ diff --git a/game/images/music_room/volumeHover.png b/game/images/music_room/volumeHover.png new file mode 100644 index 0000000..9704410 Binary files /dev/null and b/game/images/music_room/volumeHover.png differ diff --git a/game/images/music_room/volumeOn.png b/game/images/music_room/volumeOn.png new file mode 100644 index 0000000..17ed1a7 Binary files /dev/null and b/game/images/music_room/volumeOn.png differ diff --git a/game/images/music_room/volumeOnHover.png b/game/images/music_room/volumeOnHover.png new file mode 100644 index 0000000..6109432 Binary files /dev/null and b/game/images/music_room/volumeOnHover.png differ diff --git a/game/manualtracks.rpy b/game/manualtracks.rpy new file mode 100644 index 0000000..6b557de --- /dev/null +++ b/game/manualtracks.rpy @@ -0,0 +1,21 @@ +# This RPY is a base template on defining songs manually that aren't located in +# the track folder. Use the commented sample below as a base to manually +# add songs from your projects to here. + +init python: + # imports the OST library. Leave this as-is. + import ost + + ## Base Template + ###################################### + + # easy_like_summer = ost.soundtrack( + # name = "Easy", + # path = "bgm/09 Easy.mp3", + # priority = 1, + # author = "Lionel Richie", + # description = "Easy like sunday morning.", + # cover_art = False, + # unlocked = renpy.seen_audio("bgm/09 Easy.mp3") + # ) + # ost.manualDefineList.append(easy_like_summer) diff --git a/game/music_screen.rpy b/game/music_screen.rpy new file mode 100644 index 0000000..d56b0c2 --- /dev/null +++ b/game/music_screen.rpy @@ -0,0 +1,309 @@ + +## Music Room ######################################################## +## +## This controls the music room player positions, sizes and more. +###################################################################### + +## The positions and sizes of the music viewport list +define gui.music_room_viewport_xsize = int(250 * ost.scale) +define gui.music_room_viewport_pos = int(20 * ost.scale) +define gui.music_room_spacing = int(20 * ost.scale) +define gui.music_room_viewport_ysize = 0.93 + +## The positions and sizes of the music information text +define gui.music_room_information_xpos = int(700 * ost.scale) +define gui.music_room_information_ypos = int(208 * ost.scale) +define gui.music_room_information_xsize = int(570 * ost.scale) + +## The positions and sizes of the music controls +define gui.music_room_options_xpos = int(715 * ost.scale) +define gui.music_room_options_ypos = int(410 * ost.scale) +define gui.music_room_options_spacing = int(20 * ost.scale) +define gui.music_room_options_button_size = int(36 * ost.scale) + +## The positions of the music settings controls +define gui.music_room_settings_ypos = int(450 * ost.scale) + +## The positions and sizes of the music progress bar +define gui.music_room_progress_xsize = int(710 * ost.scale) +define gui.music_room_progress_xpos = int(330 * ost.scale) +define gui.music_room_progress_ypos = int(520 * ost.scale) + +## The positions and sizes of the music volume bar +define gui.music_room_volume_xsize = int(120 * ost.scale) +define gui.music_room_volume_xpos = int(1130 * ost.scale) +define gui.music_room_volume_options_xpos = int(1090 * ost.scale) +define gui.music_room_volume_options_ypos = int(509 * ost.scale) + +## The positions for the music progress/duration time text +define gui.music_room_progress_text_xpos = int(330 * ost.scale) +define gui.music_room_text_size = gui.interface_text_size +define gui.music_room_progress_text_xalign = 0.28 * ost.scale +define gui.music_room_progress_text_yalign = 0.79 * ost.scale + +## The positions for the cover art and it's transform properties +define gui.music_room_cover_art_xpos = int(500 * ost.scale) +define gui.music_room_cover_art_ypos = int(300 * ost.scale) +define gui.music_room_cover_art_size = int(350 * ost.scale) + +init python: + + if renpy.variant('small'): + gui.music_room_text_size = int(24 * ost.scale) + gui.music_room_progress_text_yalign = 0.81 * ost.scale + gui.music_room_volume_options_xpos = int(1080 * ost.scale) + gui.music_room_volume_options_ypos = int(514 * ost.scale) + gui.music_room_options_button_size = int(40 * ost.scale) + +image readablePos = DynamicDisplayable(renpy.curry(ost.music_pos)( + "music_room_progress_text")) +image readableDur = DynamicDisplayable(renpy.curry(ost.music_dur)( + "music_room_duration_text")) +image titleName = DynamicDisplayable(renpy.curry(ost.dynamic_title_text)( + "music_room_information_text")) +image authorName = DynamicDisplayable(renpy.curry(ost.dynamic_author_text)( + "music_room_information_text")) +image coverArt = DynamicDisplayable(ost.refresh_cover_data) +image songDescription = DynamicDisplayable(renpy.curry(ost.dynamic_description_text)( + "music_room_information_text")) +image rpa_map_warning = DynamicDisplayable(renpy.curry(ost.rpa_mapping_detection)( + "music_room_information_text")) +image playPauseButton = DynamicDisplayable(ost.auto_play_pause_button) + +screen music_room(): + + tag menu + + default bar_val = ost.AdjustableAudioPositionValue() + + style_prefix "music_room" + + add gui.main_menu_background + + frame: + style "music_room_frame" + + side "c l": + + viewport id "vpo": + + style "music_room_viewport" + + mousewheel True + has vbox + + spacing gui.navigation_spacing + + for st in ost.soundtracks: + textbutton "[st.name]": + text_style "music_room_list_button" + if ost.game_soundtrack: + action [SensitiveIf(ost.game_soundtrack.name != st.name + or ost.game_soundtrack.author != st.author + or ost.game_soundtrack.description != st.description), + SetVariable("ost.game_soundtrack", st), + SetVariable("ost.pausedstate", False), + Play("music_room", st.path, loop=ost.loopSong, + fadein=2.0)] + else: + action [SetVariable("ost.game_soundtrack", st), + SetVariable("ost.pausedstate", False), + Play("music_room", st.path, loop=ost.loopSong, fadein=2.0)] + + vbar value YScrollValue("vpo") xpos 1.0 ypos 20 + + if ost.game_soundtrack: + + if ost.game_soundtrack.cover_art: + + add "coverArt" at cover_art_fade + + if ost.game_soundtrack.author: + vbox: + hbox: + vbox: + style_prefix "music_room_information" + add "titleName" + hbox: + vbox: + style_prefix "music_room_information" + add "authorName" + + if ost.game_soundtrack.description: + hbox: + vbox: + style_prefix "music_room_information" + add "songDescription" + + hbox: + style "music_room_control_options" + + imagebutton: + idle At("images/music_room/backward.png", imagebutton_scale) + hover At("images/music_room/backwardHover.png", imagebutton_scale) + action [SensitiveIf(renpy.music.is_playing(channel='music_room')), + Function(ost.current_music_backward)] + + add "playPauseButton" at imagebutton_scale + + imagebutton: + idle At("images/music_room/forward.png", imagebutton_scale) + hover At("images/music_room/forwardHover.png", imagebutton_scale) + action [SensitiveIf(renpy.music.is_playing(channel='music_room')), + Function(ost.current_music_forward)] + + hbox: + + style "music_room_setting_options" + + imagebutton: + idle At(ConditionSwitch("ost.organizeAZ", "images/music_room/A-ZOn.png", + "True", "images/music_room/A-Z.png"), imagebutton_scale) + hover At("images/music_room/A-ZHover.png", imagebutton_scale) + action [ToggleVariable("ost.organizeAZ", False, True), + Function(ost.resort)] + + imagebutton: + idle At(ConditionSwitch("ost.organizePriority", + "images/music_room/priorityOn.png", "True", + "images/music_room/priority.png"), imagebutton_scale) + hover At("images/music_room/priorityHover.png", imagebutton_scale) + action [ToggleVariable("ost.organizePriority", False, True), + Function(ost.resort)] + + imagebutton: + idle At(ConditionSwitch("ost.loopSong", + "images/music_room/replayOn.png", "True", + "images/music_room/replay.png"), imagebutton_scale) + hover At("images/music_room/replayHover.png", imagebutton_scale) + action [ToggleVariable("ost.loopSong", False, True)] + + imagebutton: + idle At(ConditionSwitch("ost.randomSong", + "images/music_room/shuffleOn.png", "True", + "images/music_room/shuffle.png"), imagebutton_scale) + hover At("images/music_room/shuffleHover.png", imagebutton_scale) + action [ToggleVariable("ost.randomSong", False, True)] + + imagebutton: + idle At("images/music_room/refreshList.png", imagebutton_scale) + hover At("images/music_room/refreshHover.png", imagebutton_scale) + action [Function(ost.refresh_list)] + + bar: + style "music_room_progress_bar" + value bar_val + hovered bar_val.hovered + unhovered bar_val.unhovered + + bar value Preference ("music_room_mixer volume") style "music_room_volume_bar" + + imagebutton: + style "music_room_volume_options" + idle At(ConditionSwitch("preferences.get_volume(\"music_room_mixer\") == 0.0", + "images/music_room/volume.png", "True", + "images/music_room/volumeOn.png"), imagebutton_scale) + hover At(ConditionSwitch("preferences.get_volume(\"music_room_mixer\") == 0.0", + "images/music_room/volumeHover.png", "True", + "images/music_room/volumeOnHover.png"), imagebutton_scale) + action [Function(ost.mute_player)] + + add "readablePos" + add "readableDur" + + text "Ren'Py Universal Player v[ost.version]": + xalign 1.0 yalign 1.0 + xoffset -10 yoffset -10 + size gui.notify_text_size + + if not config.developer: + add "rpa_map_warning" xpos 0.23 ypos 0.85 xsize 950 + + textbutton _("Return"): + style "return_button" + action [Return(), Function(ost.check_paused_state), + If(not ost.prevTrack, None, + false=Play('music', ost.prevTrack, fadein=2.0))] + +style music_room_frame is empty +style music_room_viewport is gui_viewport +style music_room_progress_bar is gui_slider +style music_room_volume_bar is gui_slider +style music_room_volume_options is gui_button +style music_room_list_button is gui_button +style music_room_control_options is gui_button +style music_room_setting_options is gui_button +style music_room_information_text is gui_text +style music_room_progress_text is gui_text +style music_room_duration_text is gui_text + +style music_room_frame: + yfill True + + background "gui/overlay/main_menu.png" + +style music_room_list_button is default: + size gui.interface_text_size + hover_color gui.hover_color + selected_color gui.selected_color + insensitive_color gui.insensitive_color + #hover_sound gui.hover_sound + #activate_sound gui.activate_sound + line_spacing 5 + +style music_room_viewport: + xpos gui.music_room_viewport_pos + ypos gui.music_room_viewport_pos + xsize gui.music_room_viewport_xsize + ysize gui.music_room_viewport_ysize + +style music_room_information_text: + font gui.interface_text_font + xpos gui.music_room_information_xpos + ypos gui.music_room_information_ypos + +style music_room_control_options: + xpos gui.music_room_options_xpos + ypos gui.music_room_options_ypos + spacing gui.music_room_spacing + +style music_room_setting_options is music_room_control_options: + ypos gui.music_room_settings_ypos + +style music_room_progress_bar: + xsize gui.music_room_progress_xsize + xpos gui.music_room_progress_xpos + ypos gui.music_room_progress_ypos + +style music_room_volume_bar: + xsize gui.music_room_volume_xsize + xpos gui.music_room_volume_xpos + ypos gui.music_room_progress_ypos + +style music_room_volume_options: + xpos gui.music_room_volume_options_xpos + ypos gui.music_room_volume_options_ypos + +style music_room_progress_text: + font gui.interface_text_font + xalign gui.music_room_progress_text_xalign + yalign gui.music_room_progress_text_yalign + size gui.music_room_text_size + +style music_room_duration_text is music_room_progress_text: + xalign 0.79 * ost.scale + +style music_room_information_vbox: + xsize gui.music_room_information_xsize + xfill True + +transform cover_art_fade: + anchor (0.5, 0.5) + xpos gui.music_room_cover_art_xpos + ypos gui.music_room_cover_art_ypos + size (gui.music_room_cover_art_size, gui.music_room_cover_art_size) + alpha 0 + linear 0.2 alpha 1 + +transform imagebutton_scale: + size(gui.music_room_options_button_size, gui.music_room_options_button_size) \ No newline at end of file diff --git a/game/python-packages/binaries.txt b/game/python-packages/binaries.txt new file mode 100644 index 0000000..c70742a Binary files /dev/null and b/game/python-packages/binaries.txt differ diff --git a/game/python-packages/ost.py b/game/python-packages/ost.py new file mode 100644 index 0000000..5cac015 --- /dev/null +++ b/game/python-packages/ost.py @@ -0,0 +1,668 @@ +# 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') + 1.6 + + if soundtrack_position is not None: + game_soundtrack_pause = ("" + + game_soundtrack.path) + + music.stop(channel='music_room',fadeout=2.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=2.0) + else: + music.play(game_soundtrack_pause, channel = 'music_room', fadein=2.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() \ No newline at end of file diff --git a/game/python-packages/tinytag.py b/game/python-packages/tinytag.py new file mode 100644 index 0000000..b0b5428 --- /dev/null +++ b/game/python-packages/tinytag.py @@ -0,0 +1,1149 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# tinytag - an audio meta info reader +# Copyright (c) 2014-2018 Tom Wallroth +# +# Sources on github: +# http://github.com/devsnd/tinytag/ + +# MIT License + +# Copyright (c) 2014-2019 Tom Wallroth + +# 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. + + +from __future__ import print_function + +import json +from collections import MutableMapping, OrderedDict +import codecs +from functools import reduce +import struct +import os +import io +import sys +from io import BytesIO +import re + +DEBUG = os.environ.get('DEBUG', False) # some of the parsers can print debug info + + +class TinyTagException(LookupError): # inherit LookupError for backwards compat + pass + + +def _read(fh, nbytes): # helper function to check if we haven't reached EOF + b = fh.read(nbytes) + if len(b) < nbytes: + raise TinyTagException('Unexpected end of file') + return b + + +def stderr(*args): + sys.stderr.write('%s\n' % ' '.join(args)) + sys.stderr.flush() + + +def _bytes_to_int_le(b): + fmt = {1: ' 0: + return TinyTag(None, 0) + with io.open(filename, 'rb') as af: + parser_class = cls.get_parser_class(filename, af) + tag = parser_class(af, size, ignore_errors=ignore_errors) + tag.load(tags=tags, duration=duration, image=image) + return tag + + def __str__(self): + return json.dumps(OrderedDict(sorted(self.as_dict().items()))) + + def __repr__(self): + return str(self) + + def load(self, tags, duration, image=False): + self._load_image = image + if tags: + self._parse_tag(self._filehandler) + if duration: + if tags: # rewind file if the tags were already parsed + self._filehandler.seek(0) + self._determine_duration(self._filehandler) + + def _set_field(self, fieldname, bytestring, transfunc=None): + """convienience function to set fields of the tinytag by name. + the payload (bytestring) can be changed using the transfunc""" + if getattr(self, fieldname): # do not overwrite existing data + return + value = bytestring if transfunc is None else transfunc(bytestring) + if DEBUG: + stderr('Setting field "%s" to "%s"' % (fieldname, value)) + if fieldname == 'genre': + genre_id = 255 + if value.isdigit(): # funky: id3v1 genre hidden in a id3v2 field + genre_id = int(value) + else: # funkier: the TCO may contain genres in parens, e.g. '(13)' + genre_in_parens = re.match('^\\((\\d+)\\)$', value) + if genre_in_parens: + genre_id = int(genre_in_parens.group(1)) + if 0 <= genre_id < len(ID3.ID3V1_GENRES): + value = ID3.ID3V1_GENRES[genre_id] + if fieldname in ("track", "disc"): + if type(value).__name__ in ('str', 'unicode') and '/' in value: + current, total = value.split('/')[:2] + setattr(self, "%s_total" % fieldname, total) + else: + current = value + setattr(self, fieldname, current) + else: + setattr(self, fieldname, value) + + def _determine_duration(self, fh): + raise NotImplementedError() + + def _parse_tag(self, fh): + raise NotImplementedError() + + def update(self, other): + # update the values of this tag with the values from another tag + for key in ['track', 'track_total', 'title', 'artist', + 'album', 'albumartist', 'year', 'duration', + 'genre', 'disc', 'disc_total', 'comment', 'composer']: + if not getattr(self, key) and getattr(other, key): + setattr(self, key, getattr(other, key)) + + @staticmethod + def _unpad(s): + # strings in mp3 and asf *may* be terminated with a zero byte at the end + return s.replace('\x00', '') + + +class MP4(TinyTag): + # see: https://developer.apple.com/library/mac/documentation/QuickTime/QTFF/Metadata/Metadata.html + # and: https://developer.apple.com/library/mac/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html + + class Parser: + # https://developer.apple.com/library/mac/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW34 + ATOM_DECODER_BY_TYPE = { + 0: lambda x: x, # 'reserved', + 1: lambda x: codecs.decode(x, 'utf-8', 'replace'), # UTF-8 + 2: lambda x: codecs.decode(x, 'utf-16', 'replace'), # UTF-16 + 3: lambda x: codecs.decode(x, 's/jis', 'replace'), # S/JIS + # 16: duration in millis + 13: lambda x: x, # JPEG + 14: lambda x: x, # PNG + 21: lambda x: struct.unpack('>b', x)[0], # BE Signed int + 22: lambda x: struct.unpack('>B', x)[0], # BE Unsigned int + 23: lambda x: struct.unpack('>f', x)[0], # BE Float32 + 24: lambda x: struct.unpack('>d', x)[0], # BE Float64 + # 27: lambda x: x, # BMP + # 28: lambda x: x, # QuickTime Metadata atom + 65: lambda x: struct.unpack('b', x)[0], # 8-bit Signed int + 66: lambda x: struct.unpack('>h', x)[0], # BE 16-bit Signed int + 67: lambda x: struct.unpack('>i', x)[0], # BE 32-bit Signed int + 74: lambda x: struct.unpack('>q', x)[0], # BE 64-bit Signed int + 75: lambda x: struct.unpack('B', x)[0], # 8-bit Unsigned int + 76: lambda x: struct.unpack('>H', x)[0], # BE 16-bit Unsigned int + 77: lambda x: struct.unpack('>I', x)[0], # BE 32-bit Unsigned int + 78: lambda x: struct.unpack('>Q', x)[0], # BE 64-bit Unsigned int + } + + @classmethod + def make_data_atom_parser(cls, fieldname): + def parse_data_atom(data_atom): + data_type = struct.unpack('>I', data_atom[:4])[0] + conversion = cls.ATOM_DECODER_BY_TYPE.get(data_type) + if conversion is None: + stderr('Cannot convert data type: %s' % data_type) + return {} # don't know how to convert data atom + # skip header & null-bytes, convert rest + return {fieldname: conversion(data_atom[8:])} + return parse_data_atom + + @classmethod + def make_number_parser(cls, fieldname1, fieldname2): + def _(data_atom): + number_data = data_atom[8:14] + numbers = struct.unpack('>HHH', number_data) + # for some reason the first number is always irrelevant. + return {fieldname1: numbers[1], fieldname2: numbers[2]} + return _ + + @classmethod + def parse_id3v1_genre(cls, data_atom): + # dunno why the genre is offset by -1 but that's how mutagen does it + idx = struct.unpack('>H', data_atom[8:])[0] - 1 + if idx < len(ID3.ID3V1_GENRES): + return {'genre': ID3.ID3V1_GENRES[idx]} + return {'genre': None} + + @classmethod + def parse_audio_sample_entry(cls, data): + # this atom also contains the esds atom: + # https://ffmpeg.org/doxygen/0.6/mov_8c-source.html + # http://xhelmboyx.tripod.com/formats/mp4-layout.txt + datafh = BytesIO(data) + datafh.seek(16, os.SEEK_CUR) # jump over version and flags + channels = struct.unpack('>H', datafh.read(2))[0] + datafh.seek(2, os.SEEK_CUR) # jump over bit_depth + datafh.seek(2, os.SEEK_CUR) # jump over QT compr id & pkt size + sr = struct.unpack('>I', datafh.read(4))[0] + esds_atom_size = struct.unpack('>I', data[28:32])[0] + esds_atom = BytesIO(data[36:36 + esds_atom_size]) + # http://sasperger.tistory.com/103 + esds_atom.seek(22, os.SEEK_CUR) # jump over most data... + esds_atom.seek(4, os.SEEK_CUR) # jump over max bitrate + avg_br = struct.unpack('>I', esds_atom.read(4))[0] / 1000.0 # kbit/s + return {'channels': channels, 'samplerate': sr, 'bitrate': avg_br} + + @classmethod + def parse_mvhd(cls, data): + # http://stackoverflow.com/a/3639993/1191373 + walker = BytesIO(data) + version = struct.unpack('b', walker.read(1))[0] + walker.seek(3, os.SEEK_CUR) # jump over flags + if version == 0: # uses 32 bit integers for timestamps + walker.seek(8, os.SEEK_CUR) # jump over create & mod times + time_scale = struct.unpack('>I', walker.read(4))[0] + duration = struct.unpack('>I', walker.read(4))[0] + else: # version == 1: # uses 64 bit integers for timestamps + walker.seek(16, os.SEEK_CUR) # jump over create & mod times + time_scale = struct.unpack('>I', walker.read(4))[0] + duration = struct.unpack('>q', walker.read(8))[0] + return {'duration': float(duration) / time_scale} + + @classmethod + def debug_atom(cls, data): + stderr(data) # use this function to inspect atoms in an atom tree + return {} + + # The parser tree: Each key is an atom name which is traversed if existing. + # Leaves of the parser tree are callables which receive the atom data. + # callables return {fieldname: value} which is updates the TinyTag. + META_DATA_TREE = {b'moov': {b'udta': {b'meta': {b'ilst': { + # see: http://atomicparsley.sourceforge.net/mpeg-4files.html + b'\xa9alb': {b'data': Parser.make_data_atom_parser('album')}, + b'\xa9ART': {b'data': Parser.make_data_atom_parser('artist')}, + b'aART': {b'data': Parser.make_data_atom_parser('albumartist')}, + # b'cpil': {b'data': Parser.make_data_atom_parser('compilation')}, + b'\xa9cmt': {b'data': Parser.make_data_atom_parser('comment')}, + b'disk': {b'data': Parser.make_number_parser('disc', 'disc_total')}, + b'\xa9wrt': {b'data': Parser.make_data_atom_parser('composer')}, + b'\xa9day': {b'data': Parser.make_data_atom_parser('year')}, + b'\xa9gen': {b'data': Parser.make_data_atom_parser('genre')}, + b'gnre': {b'data': Parser.parse_id3v1_genre}, + b'\xa9nam': {b'data': Parser.make_data_atom_parser('title')}, + b'trkn': {b'data': Parser.make_number_parser('track', 'track_total')}, + }}}}} + + # see: https://developer.apple.com/library/mac/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html + AUDIO_DATA_TREE = { + b'moov': { + b'mvhd': Parser.parse_mvhd, + b'trak': {b'mdia': {b"minf": {b"stbl": {b"stsd": {b'mp4a': + Parser.parse_audio_sample_entry + }}}}} + } + } + + IMAGE_DATA_TREE = {b'moov': {b'udta': {b'meta': {b'ilst': { + b'covr': {b'data': Parser.make_data_atom_parser('_image_data')}, + }}}}} + + VERSIONED_ATOMS = {b'meta', b'stsd'} # those have an extra 4 byte header + FLAGGED_ATOMS = {b'stsd'} # these also have an extra 4 byte header + + def _determine_duration(self, fh): + self._traverse_atoms(fh, path=self.AUDIO_DATA_TREE) + + def _parse_tag(self, fh): + self._traverse_atoms(fh, path=self.META_DATA_TREE) + if self._load_image: # A bit inefficient, we rewind the file + self._filehandler.seek(0) # to parse it again for the image + self._traverse_atoms(fh, path=self.IMAGE_DATA_TREE) + + def _traverse_atoms(self, fh, path, stop_pos=None, curr_path=None): + header_size = 8 + atom_header = fh.read(header_size) + while len(atom_header) == header_size: + atom_size = struct.unpack('>I', atom_header[:4])[0] - header_size + atom_type = atom_header[4:] + if curr_path is None: # keep track how we traversed in the tree + curr_path = [atom_type] + if atom_size <= 0: # empty atom, jump to next one + atom_header = fh.read(header_size) + continue + if DEBUG: + stderr('%s pos: %d atom: %s len: %d' % (' ' * 4 * len(curr_path), fh.tell() - header_size, atom_type, atom_size + header_size)) + if atom_type in self.VERSIONED_ATOMS: # jump atom version for now + fh.seek(4, os.SEEK_CUR) + if atom_type in self.FLAGGED_ATOMS: # jump atom flags for now + fh.seek(4, os.SEEK_CUR) + sub_path = path.get(atom_type, None) + # if the path leaf is a dict, traverse deeper into the tree: + if issubclass(type(sub_path), MutableMapping): + atom_end_pos = fh.tell() + atom_size + self._traverse_atoms(fh, path=sub_path, stop_pos=atom_end_pos, + curr_path=curr_path + [atom_type]) + # if the path-leaf is a callable, call it on the atom data + elif callable(sub_path): + for fieldname, value in sub_path(fh.read(atom_size)).items(): + if DEBUG: + stderr(' ' * 4 * len(curr_path), 'FIELD: ', fieldname) + if fieldname: + self._set_field(fieldname, value) + # if no action was specified using dict or callable, jump over atom + else: + fh.seek(atom_size, os.SEEK_CUR) + # check if we have reached the end of this branch: + if stop_pos and fh.tell() >= stop_pos: + return # return to parent (next parent node in tree) + atom_header = fh.read(header_size) # read next atom + + +class ID3(TinyTag): + FRAME_ID_TO_FIELD = { # Mapping from Frame ID to a field of the TinyTag + 'COMM': 'comment', 'COM': 'comment', + 'TRCK': 'track', 'TRK': 'track', + 'TYER': 'year', 'TYE': 'year', + 'TALB': 'album', 'TAL': 'album', + 'TPE1': 'artist', 'TP1': 'artist', + 'TIT2': 'title', 'TT2': 'title', + 'TCON': 'genre', 'TCO': 'genre', + 'TPOS': 'disc', + 'TPE2': 'albumartist', 'TCOM': 'composer', + } + IMAGE_FRAME_IDS = {'APIC', 'PIC'} + PARSABLE_FRAME_IDS = set(FRAME_ID_TO_FIELD.keys()).union(IMAGE_FRAME_IDS) + _MAX_ESTIMATION_SEC = 30 + _CBR_DETECTION_FRAME_COUNT = 5 + _USE_XING_HEADER = True # much faster, but can be deactivated for testing + + ID3V1_GENRES = [ + 'Blues', 'Classic Rock', 'Country', 'Dance', 'Disco', + 'Funk', 'Grunge', 'Hip-Hop', 'Jazz', 'Metal', 'New Age', 'Oldies', + 'Other', 'Pop', 'R&B', 'Rap', 'Reggae', 'Rock', 'Techno', 'Industrial', + 'Alternative', 'Ska', 'Death Metal', 'Pranks', 'Soundtrack', + 'Euro-Techno', 'Ambient', 'Trip-Hop', 'Vocal', 'Jazz+Funk', 'Fusion', + 'Trance', 'Classical', 'Instrumental', 'Acid', 'House', 'Game', + 'Sound Clip', 'Gospel', 'Noise', 'AlternRock', 'Bass', 'Soul', 'Punk', + 'Space', 'Meditative', 'Instrumental Pop', 'Instrumental Rock', + 'Ethnic', 'Gothic', 'Darkwave', 'Techno-Industrial', 'Electronic', + 'Pop-Folk', 'Eurodance', 'Dream', 'Southern Rock', 'Comedy', 'Cult', + 'Gangsta', 'Top 40', 'Christian Rap', 'Pop/Funk', 'Jungle', + 'Native American', 'Cabaret', 'New Wave', 'Psychadelic', 'Rave', + 'Showtunes', 'Trailer', 'Lo-Fi', 'Tribal', 'Acid Punk', 'Acid Jazz', + 'Polka', 'Retro', 'Musical', 'Rock & Roll', 'Hard Rock', + + # Wimamp Extended Genres + 'Folk', 'Folk-Rock', 'National Folk', 'Swing', 'Fast Fusion', 'Bebob', + 'Latin', 'Revival', 'Celtic', 'Bluegrass', 'Avantgarde', 'Gothic Rock', + 'Progressive Rock', 'Psychedelic Rock', 'Symphonic Rock', 'Slow Rock', + 'Big Band', 'Chorus', 'Easy Listening', 'Acoustic', 'Humour', 'Speech', + 'Chanson', 'Opera', 'Chamber Music', 'Sonata', 'Symphony', 'Booty Bass', + 'Primus', 'Porn Groove', 'Satire', 'Slow Jam', 'Club', 'Tango', 'Samba', + 'Folklore', 'Ballad', 'Power Ballad', 'Rhythmic Soul', 'Freestyle', + 'Duet', 'Punk Rock', 'Drum Solo', 'A capella', 'Euro-House', + 'Dance Hall', 'Goa', 'Drum & Bass', + + # according to https://de.wikipedia.org/wiki/Liste_der_ID3v1-Genres: + 'Club-House', 'Hardcore Techno', 'Terror', 'Indie', 'BritPop', + '', # don't use ethnic slur ("Negerpunk", WTF!) + 'Polsk Punk', 'Beat', 'Christian Gangsta Rap', 'Heavy Metal', + 'Black Metal', 'Contemporary Christian', 'Christian Rock', + # WinAmp 1.91 + 'Merengue', 'Salsa', 'Thrash Metal', 'Anime', 'Jpop', 'Synthpop', + # WinAmp 5.6 + 'Abstract', 'Art Rock', 'Baroque', 'Bhangra', 'Big Beat', 'Breakbeat', + 'Chillout', 'Downtempo', 'Dub', 'EBM', 'Eclectic', 'Electro', + 'Electroclash', 'Emo', 'Experimental', 'Garage', 'Illbient', + 'Industro-Goth', 'Jam Band', 'Krautrock', 'Leftfield', 'Lounge', + 'Math Rock', 'New Romantic', 'Nu-Breakz', 'Post-Punk', 'Post-Rock', + 'Psytrance', 'Shoegaze', 'Space Rock', 'Trop Rock', 'World Music', + 'Neoclassical', 'Audiobook', 'Audio Theatre', 'Neue Deutsche Welle', + 'Podcast', 'Indie Rock', 'G-Funk', 'Dubstep', 'Garage Rock', 'Psybient', + ] + + def __init__(self, filehandler, filesize, *args, **kwargs): + TinyTag.__init__(self, filehandler, filesize, *args, **kwargs) + # save position after the ID3 tag for duration mesurement speedup + self._bytepos_after_id3v2 = 0 + + @classmethod + def set_estimation_precision(cls, estimation_in_seconds): + cls._MAX_ESTIMATION_SEC = estimation_in_seconds + + # see this page for the magic values used in mp3: + # http://www.mpgedit.org/mpgedit/mpeg_format/mpeghdr.htm + samplerates = [ + [11025, 12000, 8000], # MPEG 2.5 + [], # reserved + [22050, 24000, 16000], # MPEG 2 + [44100, 48000, 32000], # MPEG 1 + ] + v1l1 = [0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 0] + v1l2 = [0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 0] + v1l3 = [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0] + v2l1 = [0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 0] + v2l2 = [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0] + v2l3 = v2l2 + bitrate_by_version_by_layer = [ + [None, v2l3, v2l2, v2l1], # MPEG Version 2.5 # note that the layers go + None, # reserved # from 3 to 1 by design. + [None, v2l3, v2l2, v2l1], # MPEG Version 2 # the first layer id is + [None, v1l3, v1l2, v1l1], # MPEG Version 1 # reserved + ] + samples_per_frame = 1152 # the default frame size for mp3 + channels_per_channel_mode = [ + 2, # 00 Stereo + 2, # 01 Joint stereo (Stereo) + 2, # 10 Dual channel (2 mono channels) + 1, # 11 Single channel (Mono) + ] + + @staticmethod + def _parse_xing_header(fh): + # see: http://www.mp3-tech.org/programmer/sources/vbrheadersdk.zip + fh.seek(4, os.SEEK_CUR) # read over Xing header + header_flags = struct.unpack('>i', fh.read(4))[0] + frames = byte_count = toc = vbr_scale = None + if header_flags & 1: # FRAMES FLAG + frames = struct.unpack('>i', fh.read(4))[0] + if header_flags & 2: # BYTES FLAG + byte_count = struct.unpack('>i', fh.read(4))[0] + if header_flags & 4: # TOC FLAG + toc = [struct.unpack('>i', fh.read(4))[0] for _ in range(100)] + if header_flags & 8: # VBR SCALE FLAG + vbr_scale = struct.unpack('>i', fh.read(4))[0] + return frames, byte_count, toc, vbr_scale + + def _determine_duration(self, fh): + max_estimation_frames = (ID3._MAX_ESTIMATION_SEC * 44100) // ID3.samples_per_frame + frame_size_accu = 0 + header_bytes = 4 + frames = 0 # count frames for determining mp3 duration + bitrate_accu = 0 # add up bitrates to find average bitrate to detect + last_bitrates = [] # CBR mp3s (multiple frames with same bitrates) + # seek to first position after id3 tag (speedup for large header) + fh.seek(self._bytepos_after_id3v2) + while True: + # reading through garbage until 11 '1' sync-bits are found + b = fh.peek(4) + if len(b) < 4: + break # EOF + sync, conf, bitrate_freq, rest = struct.unpack('BBBB', b[0:4]) + br_id = (bitrate_freq >> 4) & 0x0F # biterate id + sr_id = (bitrate_freq >> 2) & 0x03 # sample rate id + padding = 1 if bitrate_freq & 0x02 > 0 else 0 + mpeg_id = (conf >> 3) & 0x03 + layer_id = (conf >> 1) & 0x03 + channel_mode = (rest >> 6) & 0x03 + # check for eleven 1s, validate bitrate and sample rate + if not b[:2] > b'\xFF\xE0' or br_id > 14 or br_id == 0 or sr_id == 3 or layer_id == 0 or mpeg_id == 1: + idx = b.find(b'\xFF', 1) # invalid frame, find next sync header + if idx == -1: + idx = len(b) # not found: jump over the current peek buffer + fh.seek(max(idx, 1), os.SEEK_CUR) + continue + try: + self.channels = self.channels_per_channel_mode[channel_mode] + frame_bitrate = ID3.bitrate_by_version_by_layer[mpeg_id][layer_id][br_id] + self.samplerate = ID3.samplerates[mpeg_id][sr_id] + except (IndexError, TypeError): + raise TinyTagException('mp3 parsing failed') + # There might be a xing header in the first frame that contains + # all the info we need, otherwise parse multiple frames to find the + # accurate average bitrate + if frames == 0 and ID3._USE_XING_HEADER: + xing_header_offset = b.find(b'Xing') + if xing_header_offset != -1: + fh.seek(xing_header_offset, os.SEEK_CUR) + xframes, byte_count, toc, vbr_scale = ID3._parse_xing_header(fh) + if xframes and xframes != 0 and byte_count: + self.duration = xframes * ID3.samples_per_frame / float(self.samplerate) + self.bitrate = int(byte_count * 8 / self.duration / 1000) + self.audio_offset = fh.tell() + return + continue + + frames += 1 # it's most probably an mp3 frame + bitrate_accu += frame_bitrate + if frames == 1: + self.audio_offset = fh.tell() + if frames <= ID3._CBR_DETECTION_FRAME_COUNT: + last_bitrates.append(frame_bitrate) + fh.seek(4, os.SEEK_CUR) # jump over peeked bytes + + frame_length = (144000 * frame_bitrate) // self.samplerate + padding + frame_size_accu += frame_length + # if bitrate does not change over time its probably CBR + is_cbr = (frames == ID3._CBR_DETECTION_FRAME_COUNT and + len(set(last_bitrates)) == 1) + if frames == max_estimation_frames or is_cbr: + # try to estimate duration + fh.seek(-128, 2) # jump to last byte (leaving out id3v1 tag) + audio_stream_size = fh.tell() - self.audio_offset + est_frame_count = audio_stream_size / (frame_size_accu / float(frames)) + samples = est_frame_count * ID3.samples_per_frame + self.duration = samples / float(self.samplerate) + self.bitrate = int(bitrate_accu / frames) + return + + if frame_length > 1: # jump over current frame body + fh.seek(frame_length - header_bytes, os.SEEK_CUR) + if self.samplerate: + self.duration = frames * ID3.samples_per_frame / float(self.samplerate) + + def _parse_tag(self, fh): + self._parse_id3v2(fh) + attrs = ['track', 'track_total', 'title', 'artist', 'album', 'albumartist', 'year', 'genre'] + has_all_tags = all(getattr(self, attr) for attr in attrs) + if not has_all_tags and self.filesize > 128: + fh.seek(-128, os.SEEK_END) # try parsing id3v1 in last 128 bytes + self._parse_id3v1(fh) + + def _parse_id3v2(self, fh): + # for info on the specs, see: http://id3.org/Developer%20Information + header = struct.unpack('3sBBB4B', _read(fh, 10)) + tag = codecs.decode(header[0], 'ISO-8859-1') + # check if there is an ID3v2 tag at the beginning of the file + if tag == 'ID3': + major, rev = header[1:3] + if DEBUG: + stderr('Found id3 v2.%s' % major) + # unsync = (header[3] & 0x80) > 0 + extended = (header[3] & 0x40) > 0 + # experimental = (header[3] & 0x20) > 0 + # footer = (header[3] & 0x10) > 0 + size = self._calc_size(header[4:8], 7) + self._bytepos_after_id3v2 = size + end_pos = fh.tell() + size + parsed_size = 0 + if extended: # just read over the extended header. + size_bytes = struct.unpack('4B', _read(fh, 6)[0:4]) + extd_size = self._calc_size(size_bytes, 7) + fh.seek(extd_size - 6, os.SEEK_CUR) # jump over extended_header + while parsed_size < size: + frame_size = self._parse_frame(fh, id3version=major) + if frame_size == 0: + break + parsed_size += frame_size + fh.seek(end_pos, os.SEEK_SET) + + def _parse_id3v1(self, fh): + if fh.read(3) == b'TAG': # check if this is an ID3 v1 tag + def asciidecode(x): + return self._unpad(codecs.decode(x, 'latin1')) + fields = fh.read(30 + 30 + 30 + 4 + 30 + 1) + self._set_field('title', fields[:30], transfunc=asciidecode) + self._set_field('artist', fields[30:60], transfunc=asciidecode) + self._set_field('album', fields[60:90], transfunc=asciidecode) + self._set_field('year', fields[90:94], transfunc=asciidecode) + comment = fields[94:124] + if b'\x00\x00' < comment[-2:] < b'\x01\x00': + self._set_field('track', str(ord(comment[-1:]))) + comment = comment[:-2] + self._set_field('comment', comment, transfunc=asciidecode) + genre_id = ord(fields[124:125]) + if genre_id < len(ID3.ID3V1_GENRES): + self.genre = ID3.ID3V1_GENRES[genre_id] + + def _parse_frame(self, fh, id3version=False): + # ID3v2.2 especially ugly. see: http://id3.org/id3v2-00 + frame_header_size = 6 if id3version == 2 else 10 + frame_size_bytes = 3 if id3version == 2 else 4 + binformat = '3s3B' if id3version == 2 else '4s4B2B' + bits_per_byte = 7 if id3version == 4 else 8 # only id3v2.4 is synchsafe + frame_header_data = fh.read(frame_header_size) + if len(frame_header_data) != frame_header_size: + return 0 + frame = struct.unpack(binformat, frame_header_data) + frame_id = self._decode_string(frame[0]) + frame_size = self._calc_size(frame[1:1+frame_size_bytes], bits_per_byte) + if DEBUG: + stderr('Found id3 Frame %s at %d-%d of %d' % (frame_id, fh.tell(), fh.tell() + frame_size, self.filesize)) + if frame_size > 0: + # flags = frame[1+frame_size_bytes:] # dont care about flags. + if frame_id not in ID3.PARSABLE_FRAME_IDS: # jump over unparsable frames + fh.seek(frame_size, os.SEEK_CUR) + return frame_size + content = fh.read(frame_size) + fieldname = ID3.FRAME_ID_TO_FIELD.get(frame_id) + if fieldname: + self._set_field(fieldname, content, self._decode_string) + elif frame_id in self.IMAGE_FRAME_IDS and self._load_image: + # See section 4.14: http://id3.org/id3v2.4.0-frames + if frame_id == 'PIC': # ID3 v2.2: + desc_end_pos = content.index(b'\x00', 1) + 1 + else: # ID3 v2.3+ + mimetype_end_pos = content.index(b'\x00', 1) + 1 + desc_start_pos = mimetype_end_pos + 1 # jump over picture type + desc_end_pos = content.index(b'\x00', desc_start_pos) + 1 + if content[desc_end_pos:desc_end_pos+1] == b'\x00': + desc_end_pos += 1 # the description ends with 1 or 2 null bytes + self._image_data = content[desc_end_pos:] + return frame_size + return 0 + + def _decode_string(self, bytestr): + try: # it's not my fault, this is the spec. + first_byte = bytestr[:1] + if first_byte == b'\x00': # ISO-8859-1 + bytestr = bytestr[1:] + encoding = 'ISO-8859-1' + elif first_byte == b'\x01': # UTF-16 with BOM + bytestr = bytestr[1:] + if bytestr[:5] == b'eng\xff\xfe': + bytestr = bytestr[3:] # remove language (but leave BOM) + if bytestr[:5] == b'eng\xfe\xff': + bytestr = bytestr[3:] # remove language (but leave BOM) + if bytestr[:4] == b'eng\x00': + bytestr = bytestr[4:] # remove language + if bytestr[:1] == b'\x00': + bytestr = bytestr[1:] # strip optional additional null byte + # read byte order mark to determine endianess + encoding = 'UTF-16be' if bytestr[0:2] == b'\xfe\xff' else 'UTF-16le' + # strip the bom and optional null bytes + bytestr = bytestr[2:] if len(bytestr) % 2 == 0 else bytestr[2:-1] + # remove ADDITIONAL EXTRA BOM :facepalm: + if bytestr[:4] == b'\x00\x00\xff\xfe': + bytestr = bytestr[4:] + elif first_byte == b'\x02': # UTF-16LE + # strip optional null byte, if byte count uneven + bytestr = bytestr[1:-1] if len(bytestr) % 2 == 0 else bytestr[1:] + encoding = 'UTF-16le' + elif first_byte == b'\x03': # UTF-8 + bytestr = bytestr[1:] + encoding = 'UTF-8' + else: + bytestr = bytestr + encoding = 'ISO-8859-1' # wild guess + if bytestr[:4] == b'eng\x00': + bytestr = bytestr[4:] # remove language + errors = 'ignore' if self._ignore_errors else 'strict' + return self._unpad(codecs.decode(bytestr, encoding, errors)) + except UnicodeDecodeError: + raise TinyTagException('Error decoding ID3 Tag!') + + def _calc_size(self, bytestr, bits_per_byte): + # length of some mp3 header fields is described by 7 or 8-bit-bytes + return reduce(lambda accu, elem: (accu << bits_per_byte) + elem, bytestr, 0) + + +class Ogg(TinyTag): + def __init__(self, filehandler, filesize, *args, **kwargs): + TinyTag.__init__(self, filehandler, filesize, *args, **kwargs) + self._tags_parsed = False + self._max_samplenum = 0 # maximum sample position ever read + + def _determine_duration(self, fh): + max_page_size = 65536 # https://xiph.org/ogg/doc/libogg/ogg_page.html + if not self._tags_parsed: + self._parse_tag(fh) # determine sample rate + fh.seek(0) # and rewind to start + if self.filesize > max_page_size: + fh.seek(-max_page_size, 2) # go to last possible page position + while True: + b = fh.peek(4) + if len(b) == 0: + return # EOF + if b[:4] == b'OggS': # look for an ogg header + for _ in self._parse_pages(fh): + pass # parse all remaining pages + self.duration = self._max_samplenum / float(self.samplerate) + else: + idx = b.find(b'OggS') # try to find header in peeked data + seekpos = idx if idx != -1 else len(b) - 3 + fh.seek(max(seekpos, 1), os.SEEK_CUR) + + def _parse_tag(self, fh): + page_start_pos = fh.tell() # set audio_offest later if its audio data + for packet in self._parse_pages(fh): + walker = BytesIO(packet) + if packet[0:7] == b"\x01vorbis": + (channels, self.samplerate, max_bitrate, bitrate, + min_bitrate) = struct.unpack(" + # ---------------------------------------------- + # H | <16> The minimum block size (in samples) + # H | <16> The maximum block size (in samples) + # 3s | <24> The minimum frame size (in bytes) + # 3s | <24> The maximum frame size (in bytes) + # 8B | <20> Sample rate in Hz. + # | <3> (number of channels)-1. + # | <5> (bits per sample)-1. + # | <36> Total samples in stream. + # 16s| <128> MD5 signature + # min_blk, max_blk, min_frm, max_frm = header[0:4] + # min_frm = _bytes_to_int(struct.unpack('3B', min_frm)) + # max_frm = _bytes_to_int(struct.unpack('3B', max_frm)) + # channels--. bits total samples + # |----- samplerate -----| |-||----| |---------~ ~----| + # 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 + # #---4---# #---5---# #---6---# #---7---# #--8-~ ~-12-# + self.samplerate = _bytes_to_int(header[4:7]) >> 4 + self.channels = ((header[6] >> 1) & 0x07) + 1 + # bit_depth = ((header[6] & 1) << 4) + ((header[7] & 0xF0) >> 4) + # bit_depth = (bit_depth + 1) + total_sample_bytes = [(header[7] & 0x0F)] + list(header[8:12]) + total_samples = _bytes_to_int(total_sample_bytes) + self.duration = float(total_samples) / self.samplerate + if self.duration > 0: + self.bitrate = self.filesize / self.duration * 8 / 1024 + elif block_type == Flac.METADATA_VORBIS_COMMENT and not skip_tags: + oggtag = Ogg(fh, 0) + oggtag._parse_vorbis_comment(fh) + self.update(oggtag) + elif block_type >= 127: + return # invalid block type + else: + fh.seek(size, 1) # seek over this block + + if is_last_block: + return + header_data = fh.read(4) + + +class Wma(TinyTag): + ASF_CONTENT_DESCRIPTION_OBJECT = b'3&\xb2u\x8ef\xcf\x11\xa6\xd9\x00\xaa\x00b\xcel' + ASF_EXTENDED_CONTENT_DESCRIPTION_OBJECT = b'@\xa4\xd0\xd2\x07\xe3\xd2\x11\x97\xf0\x00\xa0\xc9^\xa8P' + STREAM_BITRATE_PROPERTIES_OBJECT = b'\xceu\xf8{\x8dF\xd1\x11\x8d\x82\x00`\x97\xc9\xa2\xb2' + ASF_FILE_PROPERTY_OBJECT = b'\xa1\xdc\xab\x8cG\xa9\xcf\x11\x8e\xe4\x00\xc0\x0c Se' + ASF_STREAM_PROPERTIES_OBJECT = b'\x91\x07\xdc\xb7\xb7\xa9\xcf\x11\x8e\xe6\x00\xc0\x0c Se' + STREAM_TYPE_ASF_AUDIO_MEDIA = b'@\x9ei\xf8M[\xcf\x11\xa8\xfd\x00\x80_\\D+' + # see: + # http://web.archive.org/web/20131203084402/http://msdn.microsoft.com/en-us/library/bb643323.aspx + # and (japanese, but none the less helpful) + # http://uguisu.skr.jp/Windows/format_asf.html + + def __init__(self, filehandler, filesize, *args, **kwargs): + TinyTag.__init__(self, filehandler, filesize, *args, **kwargs) + self.__tag_parsed = False + + def _determine_duration(self, fh): + if not self.__tag_parsed: + self._parse_tag(fh) + + def read_blocks(self, fh, blocks): + # blocks are a list(tuple('fieldname', byte_count, cast_int), ...) + decoded = {} + for block in blocks: + val = fh.read(block[1]) + if block[2]: + val = _bytes_to_int_le(val) + decoded[block[0]] = val + return decoded + + def __bytes_to_guid(self, obj_id_bytes): + return '-'.join([ + hex(_bytes_to_int_le(obj_id_bytes[:-12]))[2:].zfill(6), + hex(_bytes_to_int_le(obj_id_bytes[-12:-10]))[2:].zfill(4), + hex(_bytes_to_int_le(obj_id_bytes[-10:-8]))[2:].zfill(4), + hex(_bytes_to_int(obj_id_bytes[-8:-6]))[2:].zfill(4), + hex(_bytes_to_int(obj_id_bytes[-6:]))[2:].zfill(12), + ]) + + def __decode_string(self, bytestring): + return self._unpad(codecs.decode(bytestring, 'utf-16')) + + def __decode_ext_desc(self, value_type, value): + """ decode ASF_EXTENDED_CONTENT_DESCRIPTION_OBJECT values""" + if value_type == 0: # Unicode string + return self.__decode_string(value) + elif value_type == 1: # BYTE array + return value + elif 1 < value_type < 6: # DWORD / QWORD / WORD + return _bytes_to_int_le(value) + + def _parse_tag(self, fh): + self.__tag_parsed = True + guid = fh.read(16) # 128 bit GUID + if guid != b'0&\xb2u\x8ef\xcf\x11\xa6\xd9\x00\xaa\x00b\xcel': + return # not a valid ASF container! see: http://www.garykessler.net/library/file_sigs.html + struct.unpack('Q', fh.read(8))[0] # size + struct.unpack('I', fh.read(4))[0] # obj_count + if fh.read(2) != b'\x01\x02': + # http://web.archive.org/web/20131203084402/http://msdn.microsoft.com/en-us/library/bb643323.aspx#_Toc521913958 + return # not a valid asf header! + while True: + object_id = fh.read(16) + object_size = _bytes_to_int_le(fh.read(8)) + if object_size == 0 or object_size > self.filesize: + break # invalid object, stop parsing. + if object_id == Wma.ASF_CONTENT_DESCRIPTION_OBJECT: + len_blocks = self.read_blocks(fh, [ + ('title_length', 2, True), + ('author_length', 2, True), + ('copyright_length', 2, True), + ('description_length', 2, True), + ('rating_length', 2, True), + ]) + data_blocks = self.read_blocks(fh, [ + ('title', len_blocks['title_length'], False), + ('artist', len_blocks['author_length'], False), + ('', len_blocks['copyright_length'], True), + ('comment', len_blocks['description_length'], False), + ('', len_blocks['rating_length'], True), + ]) + for field_name, bytestring in data_blocks.items(): + if field_name: + self._set_field(field_name, bytestring, self.__decode_string) + elif object_id == Wma.ASF_EXTENDED_CONTENT_DESCRIPTION_OBJECT: + mapping = { + 'WM/TrackNumber': 'track', + 'WM/PartOfSet': 'disc', + 'WM/Year': 'year', + 'WM/AlbumArtist': 'albumartist', + 'WM/Genre': 'genre', + 'WM/AlbumTitle': 'album', + 'WM/Composer': 'composer', + } + # see: http://web.archive.org/web/20131203084402/http://msdn.microsoft.com/en-us/library/bb643323.aspx#_Toc509555195 + descriptor_count = _bytes_to_int_le(fh.read(2)) + for _ in range(descriptor_count): + name_len = _bytes_to_int_le(fh.read(2)) + name = self.__decode_string(fh.read(name_len)) + value_type = _bytes_to_int_le(fh.read(2)) + value_len = _bytes_to_int_le(fh.read(2)) + value = fh.read(value_len) + field_name = mapping.get(name) + if field_name: + field_value = self.__decode_ext_desc(value_type, value) + self._set_field(field_name, field_value) + elif object_id == Wma.ASF_FILE_PROPERTY_OBJECT: + blocks = self.read_blocks(fh, [ + ('file_id', 16, False), + ('file_size', 8, False), + ('creation_date', 8, True), + ('data_packets_count', 8, True), + ('play_duration', 8, True), + ('send_duration', 8, True), + ('preroll', 8, True), + ('flags', 4, False), + ('minimum_data_packet_size', 4, True), + ('maximum_data_packet_size', 4, True), + ('maximum_bitrate', 4, False), + ]) + self.duration = blocks.get('play_duration') / float(10000000) + elif object_id == Wma.ASF_STREAM_PROPERTIES_OBJECT: + blocks = self.read_blocks(fh, [ + ('stream_type', 16, False), + ('error_correction_type', 16, False), + ('time_offset', 8, True), + ('type_specific_data_length', 4, True), + ('error_correction_data_length', 4, True), + ('flags', 2, True), + ('reserved', 4, False) + ]) + already_read = 0 + if blocks['stream_type'] == Wma.STREAM_TYPE_ASF_AUDIO_MEDIA: + stream_info = self.read_blocks(fh, [ + ('codec_id_format_tag', 2, True), + ('number_of_channels', 2, True), + ('samples_per_second', 4, True), + ('avg_bytes_per_second', 4, True), + ('block_alignment', 2, True), + ('bits_per_sample', 2, True), + ]) + self.samplerate = stream_info['samples_per_second'] + self.bitrate = stream_info['avg_bytes_per_second'] * 8 / float(1000) + already_read = 16 + fh.seek(blocks['type_specific_data_length'] - already_read, os.SEEK_CUR) + fh.seek(blocks['error_correction_data_length'], os.SEEK_CUR) + else: + fh.seek(object_size - 24, os.SEEK_CUR) # read over onknown object ids diff --git a/game/screens.rpy b/game/screens.rpy index 2a489b1..2e51584 100644 --- a/game/screens.rpy +++ b/game/screens.rpy @@ -1270,7 +1270,8 @@ screen extrasnavigation(): [ "Updates", ShowMenu("updates") ], [ "Gallery", ShowMenu("cg_gallery_0") ], [ "Mods", ShowMenu("mod_menu") ], - [ "Return", ShowMenu("main_menu") ] + [ "Return", ShowMenu("main_menu") ], + [ "OST Player", [ShowMenu("music_room"), Function(ost.get_music_channel_info), Stop('music', fadeout=2.0), Function(ost.refresh_list)] ] ] ) ## Help screen ################################################################# diff --git a/game/track/Amberlight Brilliance - Demo.ogg b/game/track/Amberlight Brilliance - Demo.ogg new file mode 100644 index 0000000..39d1ed8 Binary files /dev/null and b/game/track/Amberlight Brilliance - Demo.ogg differ diff --git a/game/track/Amberlight Brilliance - Live.ogg b/game/track/Amberlight Brilliance - Live.ogg new file mode 100644 index 0000000..d788493 Binary files /dev/null and b/game/track/Amberlight Brilliance - Live.ogg differ diff --git a/game/track/Amberlight Brilliance - Piano.ogg b/game/track/Amberlight Brilliance - Piano.ogg new file mode 100644 index 0000000..5aab08f Binary files /dev/null and b/game/track/Amberlight Brilliance - Piano.ogg differ diff --git a/game/track/Amberlight Brilliance D ending.ogg b/game/track/Amberlight Brilliance D ending.ogg new file mode 100644 index 0000000..7f210dd Binary files /dev/null and b/game/track/Amberlight Brilliance D ending.ogg differ diff --git a/game/track/Amberlight Brilliance.ogg b/game/track/Amberlight Brilliance.ogg new file mode 100644 index 0000000..bc59754 Binary files /dev/null and b/game/track/Amberlight Brilliance.ogg differ diff --git a/game/track/Appreciating her Company.ogg b/game/track/Appreciating her Company.ogg new file mode 100644 index 0000000..515a377 Binary files /dev/null and b/game/track/Appreciating her Company.ogg differ diff --git a/game/track/Appreciating the Atmosphere.ogg b/game/track/Appreciating the Atmosphere.ogg new file mode 100644 index 0000000..0785a64 Binary files /dev/null and b/game/track/Appreciating the Atmosphere.ogg differ diff --git a/game/track/Appreciating the Moment.ogg b/game/track/Appreciating the Moment.ogg new file mode 100644 index 0000000..6f5a1a2 Binary files /dev/null and b/game/track/Appreciating the Moment.ogg differ diff --git a/game/track/Bayside Bumming it.ogg b/game/track/Bayside Bumming it.ogg new file mode 100644 index 0000000..271be95 Binary files /dev/null and b/game/track/Bayside Bumming it.ogg differ diff --git a/game/track/Beach Chill Out.ogg b/game/track/Beach Chill Out.ogg new file mode 100644 index 0000000..7d4e909 Binary files /dev/null and b/game/track/Beach Chill Out.ogg differ diff --git a/game/track/Dino Destiny Reader.ogg b/game/track/Dino Destiny Reader.ogg new file mode 100644 index 0000000..f4b9c81 Binary files /dev/null and b/game/track/Dino Destiny Reader.ogg differ diff --git a/game/track/Dragging on and on....ogg b/game/track/Dragging on and on....ogg new file mode 100644 index 0000000..6ec9858 Binary files /dev/null and b/game/track/Dragging on and on....ogg differ diff --git a/game/track/Fuck You I Like These Weirdos.ogg b/game/track/Fuck You I Like These Weirdos.ogg new file mode 100644 index 0000000..1d18b3b Binary files /dev/null and b/game/track/Fuck You I Like These Weirdos.ogg differ diff --git a/game/track/Fuck You I Like This Chick.ogg b/game/track/Fuck You I Like This Chick.ogg new file mode 100644 index 0000000..45ce06e Binary files /dev/null and b/game/track/Fuck You I Like This Chick.ogg differ diff --git a/game/track/Fuck You I Like To Ignore Problems.ogg b/game/track/Fuck You I Like To Ignore Problems.ogg new file mode 100644 index 0000000..900f523 Binary files /dev/null and b/game/track/Fuck You I Like To Ignore Problems.ogg differ diff --git a/game/track/Fuck You I Like To Shitpost.ogg b/game/track/Fuck You I Like To Shitpost.ogg new file mode 100644 index 0000000..f9e9feb Binary files /dev/null and b/game/track/Fuck You I Like To Shitpost.ogg differ diff --git a/game/track/Many Such Cases of Being So True.ogg b/game/track/Many Such Cases of Being So True.ogg new file mode 100644 index 0000000..1b12015 Binary files /dev/null and b/game/track/Many Such Cases of Being So True.ogg differ diff --git a/game/track/Prized Bowling Ball.ogg b/game/track/Prized Bowling Ball.ogg new file mode 100644 index 0000000..54a3503 Binary files /dev/null and b/game/track/Prized Bowling Ball.ogg differ diff --git a/game/track/Prized Bowling Shoes.ogg b/game/track/Prized Bowling Shoes.ogg new file mode 100644 index 0000000..b712bcd Binary files /dev/null and b/game/track/Prized Bowling Shoes.ogg differ diff --git a/game/track/Put Music Files Here b/game/track/Put Music Files Here new file mode 100644 index 0000000..e69de29 diff --git a/game/track/Ramo de Rosas.ogg b/game/track/Ramo de Rosas.ogg new file mode 100644 index 0000000..02befe0 Binary files /dev/null and b/game/track/Ramo de Rosas.ogg differ diff --git a/game/track/Sad!.ogg b/game/track/Sad!.ogg new file mode 100644 index 0000000..ec49413 Binary files /dev/null and b/game/track/Sad!.ogg differ diff --git a/game/track/Skinrow Soul.ogg b/game/track/Skinrow Soul.ogg new file mode 100644 index 0000000..905db05 Binary files /dev/null and b/game/track/Skinrow Soul.ogg differ diff --git a/game/track/Summertime Synth.ogg b/game/track/Summertime Synth.ogg new file mode 100644 index 0000000..a5b26b7 Binary files /dev/null and b/game/track/Summertime Synth.ogg differ diff --git a/game/track/That Almost Sounded Good.ogg b/game/track/That Almost Sounded Good.ogg new file mode 100644 index 0000000..5951da4 Binary files /dev/null and b/game/track/That Almost Sounded Good.ogg differ diff --git a/game/track/That Monochromatic Weirdo.ogg b/game/track/That Monochromatic Weirdo.ogg new file mode 100644 index 0000000..4febf92 Binary files /dev/null and b/game/track/That Monochromatic Weirdo.ogg differ diff --git a/game/track/The (audiophile edition).ogg b/game/track/The (audiophile edition).ogg new file mode 100644 index 0000000..696138b Binary files /dev/null and b/game/track/The (audiophile edition).ogg differ diff --git a/game/track/The Hitler Song With The Really Long Name.ogg b/game/track/The Hitler Song With The Really Long Name.ogg new file mode 100644 index 0000000..f9a66aa Binary files /dev/null and b/game/track/The Hitler Song With The Really Long Name.ogg differ diff --git a/game/track/The Hunt for more (You)s.ogg b/game/track/The Hunt for more (You)s.ogg new file mode 100644 index 0000000..1e2f43d Binary files /dev/null and b/game/track/The Hunt for more (You)s.ogg differ diff --git a/game/track/The Top of the Social Ladder.ogg b/game/track/The Top of the Social Ladder.ogg new file mode 100644 index 0000000..fc64bd0 Binary files /dev/null and b/game/track/The Top of the Social Ladder.ogg differ diff --git a/game/track/The.ogg b/game/track/The.ogg new file mode 100644 index 0000000..4f72815 Binary files /dev/null and b/game/track/The.ogg differ diff --git a/game/track/Those Other Two Weirdos.ogg b/game/track/Those Other Two Weirdos.ogg new file mode 100644 index 0000000..4988647 Binary files /dev/null and b/game/track/Those Other Two Weirdos.ogg differ diff --git a/game/track/Tracy was fired from her real job.ogg b/game/track/Tracy was fired from her real job.ogg new file mode 100644 index 0000000..f7c8f93 Binary files /dev/null and b/game/track/Tracy was fired from her real job.ogg differ diff --git a/game/track/Venetian Values.ogg b/game/track/Venetian Values.ogg new file mode 100644 index 0000000..a002218 Binary files /dev/null and b/game/track/Venetian Values.ogg differ diff --git a/game/track/appreciating_the_scenery.ogg b/game/track/appreciating_the_scenery.ogg new file mode 100644 index 0000000..c8ac5ac Binary files /dev/null and b/game/track/appreciating_the_scenery.ogg differ diff --git a/game/track/bad_music.ogg b/game/track/bad_music.ogg new file mode 100644 index 0000000..41dc694 Binary files /dev/null and b/game/track/bad_music.ogg differ diff --git a/game/track/ballad_of_the_boot.ogg b/game/track/ballad_of_the_boot.ogg new file mode 100644 index 0000000..8b17bdc Binary files /dev/null and b/game/track/ballad_of_the_boot.ogg differ diff --git a/game/track/bayside_revamp.ogg b/game/track/bayside_revamp.ogg new file mode 100644 index 0000000..9c69dd2 Binary files /dev/null and b/game/track/bayside_revamp.ogg differ diff --git a/game/track/dinolove.ogg b/game/track/dinolove.ogg new file mode 100644 index 0000000..5aab08f Binary files /dev/null and b/game/track/dinolove.ogg differ diff --git a/game/track/fighter.ogg b/game/track/fighter.ogg new file mode 100644 index 0000000..3516062 Binary files /dev/null and b/game/track/fighter.ogg differ diff --git a/game/track/good faith leadup drums synth.ogg b/game/track/good faith leadup drums synth.ogg new file mode 100644 index 0000000..d3077cd Binary files /dev/null and b/game/track/good faith leadup drums synth.ogg differ diff --git a/game/track/good faith leadup piano.ogg b/game/track/good faith leadup piano.ogg new file mode 100644 index 0000000..c950ad9 Binary files /dev/null and b/game/track/good faith leadup piano.ogg differ diff --git a/game/track/good faith.ogg b/game/track/good faith.ogg new file mode 100644 index 0000000..2d9214b Binary files /dev/null and b/game/track/good faith.ogg differ diff --git a/game/track/goodfaithcover.png b/game/track/goodfaithcover.png new file mode 100644 index 0000000..bd40d3b Binary files /dev/null and b/game/track/goodfaithcover.png differ diff --git a/game/track/i_have_no_clue_what_im_doing.ogg b/game/track/i_have_no_clue_what_im_doing.ogg new file mode 100644 index 0000000..86f3e71 Binary files /dev/null and b/game/track/i_have_no_clue_what_im_doing.ogg differ diff --git a/game/track/intercept.ogg b/game/track/intercept.ogg new file mode 100644 index 0000000..1444bff Binary files /dev/null and b/game/track/intercept.ogg differ diff --git a/game/track/its_footloose_now.ogg b/game/track/its_footloose_now.ogg new file mode 100644 index 0000000..12ba7aa Binary files /dev/null and b/game/track/its_footloose_now.ogg differ diff --git a/game/track/its_the_original_you_didnt_ask_for.ogg b/game/track/its_the_original_you_didnt_ask_for.ogg new file mode 100644 index 0000000..546185b Binary files /dev/null and b/game/track/its_the_original_you_didnt_ask_for.ogg differ diff --git a/game/track/middle_of_nowhere.ogg b/game/track/middle_of_nowhere.ogg new file mode 100644 index 0000000..832ca78 Binary files /dev/null and b/game/track/middle_of_nowhere.ogg differ diff --git a/game/track/ost cover.png b/game/track/ost cover.png new file mode 100644 index 0000000..8e01a7c Binary files /dev/null and b/game/track/ost cover.png differ diff --git a/game/track/prom1.ogg b/game/track/prom1.ogg new file mode 100644 index 0000000..5ccfa6b Binary files /dev/null and b/game/track/prom1.ogg differ diff --git a/game/track/prom2.ogg b/game/track/prom2.ogg new file mode 100644 index 0000000..e834a59 Binary files /dev/null and b/game/track/prom2.ogg differ diff --git a/game/track/protestra_punk.ogg b/game/track/protestra_punk.ogg new file mode 100644 index 0000000..9373456 Binary files /dev/null and b/game/track/protestra_punk.ogg differ diff --git a/game/track/punk_revamp.ogg b/game/track/punk_revamp.ogg new file mode 100644 index 0000000..55e6239 Binary files /dev/null and b/game/track/punk_revamp.ogg differ diff --git a/game/track/sadist.ogg b/game/track/sadist.ogg new file mode 100644 index 0000000..8c36ce4 Binary files /dev/null and b/game/track/sadist.ogg differ diff --git a/game/track/sage goes in all fields.ogg b/game/track/sage goes in all fields.ogg new file mode 100644 index 0000000..efec723 Binary files /dev/null and b/game/track/sage goes in all fields.ogg differ diff --git a/game/track/she_fucks_human_men.ogg b/game/track/she_fucks_human_men.ogg new file mode 100644 index 0000000..122750e Binary files /dev/null and b/game/track/she_fucks_human_men.ogg differ diff --git a/game/track/this_hurts.ogg b/game/track/this_hurts.ogg new file mode 100644 index 0000000..0935485 Binary files /dev/null and b/game/track/this_hurts.ogg differ diff --git a/game/track/to_swagger.ogg b/game/track/to_swagger.ogg new file mode 100644 index 0000000..ec961bc Binary files /dev/null and b/game/track/to_swagger.ogg differ diff --git a/game/track/we just turned on the microphone in our programmers_ house.ogg b/game/track/we just turned on the microphone in our programmers_ house.ogg new file mode 100644 index 0000000..fc9ac76 Binary files /dev/null and b/game/track/we just turned on the microphone in our programmers_ house.ogg differ diff --git a/game/track/west_coast_kicking.ogg b/game/track/west_coast_kicking.ogg new file mode 100644 index 0000000..4af93c7 Binary files /dev/null and b/game/track/west_coast_kicking.ogg differ diff --git a/game/track/you can_t sage here.ogg b/game/track/you can_t sage here.ogg new file mode 100644 index 0000000..f013968 Binary files /dev/null and b/game/track/you can_t sage here.ogg differ diff --git a/game/track/you need gopher chucks.ogg b/game/track/you need gopher chucks.ogg new file mode 100644 index 0000000..9893884 Binary files /dev/null and b/game/track/you need gopher chucks.ogg differ