update to add every missing feature ever

This commit is contained in:
Yonokid
2025-06-12 00:42:29 -04:00
parent f1978cb17b
commit ec69e3f2bd
15 changed files with 1340 additions and 472 deletions

View File

@@ -41,7 +41,7 @@ jobs:
script-name: PyTaiko.py script-name: PyTaiko.py
mode: app mode: app
output-dir: . output-dir: .
include-module: raylib,moviepy,numpy,sounddevice include-module: raylib,moviepy,numpy,sounddevice,tomlkit
include-package: imageio_ffmpeg include-package: imageio_ffmpeg
noinclude-setuptools-mode: nofollow noinclude-setuptools-mode: nofollow
noinclude-IPython-mode: nofollow noinclude-IPython-mode: nofollow

1
.gitignore vendored
View File

@@ -8,3 +8,4 @@ pytaiko.build
pytaiko.dist pytaiko.dist
pytaiko.onefile-build pytaiko.onefile-build
pytaiko.exe pytaiko.exe
full.csv

View File

@@ -2,12 +2,20 @@ import sqlite3
from pathlib import Path from pathlib import Path
import pyray as ray import pyray as ray
from raylib.defines import (
RL_FUNC_ADD,
RL_ONE,
RL_ONE_MINUS_SRC_ALPHA,
RL_SRC_ALPHA,
)
from libs import song_hash
from libs.audio import audio from libs.audio import audio
from libs.utils import get_config, global_data, load_all_textures_from_zip from libs.utils import get_config, global_data, load_all_textures_from_zip
from scenes.entry import EntryScreen from scenes.entry import EntryScreen
from scenes.game import GameScreen from scenes.game import GameScreen
from scenes.result import ResultScreen from scenes.result import ResultScreen
from scenes.settings import SettingsScreen
from scenes.song_select import SongSelectScreen from scenes.song_select import SongSelectScreen
from scenes.title import TitleScreen from scenes.title import TitleScreen
@@ -18,6 +26,7 @@ class Screens:
SONG_SELECT = "SONG_SELECT" SONG_SELECT = "SONG_SELECT"
GAME = "GAME" GAME = "GAME"
RESULT = "RESULT" RESULT = "RESULT"
SETTINGS = "SETTINGS"
def create_song_db(): def create_song_db():
with sqlite3.connect('scores.db') as con: with sqlite3.connect('scores.db') as con:
@@ -33,7 +42,8 @@ def create_song_db():
ok INTEGER, ok INTEGER,
bad INTEGER, bad INTEGER,
drumroll INTEGER, drumroll INTEGER,
combo INTEGER combo INTEGER,
clear INTEGER
); );
''' '''
cursor.execute(create_table_query) cursor.execute(create_table_query)
@@ -42,6 +52,7 @@ def create_song_db():
def main(): def main():
create_song_db() create_song_db()
song_hash.song_hashes = song_hash.build_song_hashes()
screen_width: int = get_config()["video"]["screen_width"] screen_width: int = get_config()["video"]["screen_width"]
screen_height: int = get_config()["video"]["screen_height"] screen_height: int = get_config()["video"]["screen_height"]
render_width, render_height = ray.get_render_width(), ray.get_render_height() render_width, render_height = ray.get_render_width(), ray.get_render_height()
@@ -57,12 +68,12 @@ def main():
ray.set_config_flags(ray.ConfigFlags.FLAG_MSAA_4X_HINT) ray.set_config_flags(ray.ConfigFlags.FLAG_MSAA_4X_HINT)
ray.set_trace_log_level(ray.TraceLogLevel.LOG_ERROR) ray.set_trace_log_level(ray.TraceLogLevel.LOG_ERROR)
ray.set_window_max_size(screen_width, screen_height) #ray.set_window_max_size(screen_width, screen_height)
ray.set_window_min_size(screen_width, screen_height) #ray.set_window_min_size(screen_width, screen_height)
ray.init_window(screen_width, screen_height, "PyTaiko") ray.init_window(screen_width, screen_height, "PyTaiko")
if get_config()["video"]["borderless"]: if get_config()["video"]["borderless"]:
ray.toggle_borderless_windowed() ray.toggle_borderless_windowed()
ray.clear_window_state(ray.ConfigFlags.FLAG_WINDOW_TOPMOST) #ray.clear_window_state(ray.ConfigFlags.FLAG_WINDOW_TOPMOST)
if get_config()["video"]["fullscreen"]: if get_config()["video"]["fullscreen"]:
ray.maximize_window() ray.maximize_window()
@@ -76,20 +87,20 @@ def main():
song_select_screen = SongSelectScreen(screen_width, screen_height) song_select_screen = SongSelectScreen(screen_width, screen_height)
game_screen = GameScreen(screen_width, screen_height) game_screen = GameScreen(screen_width, screen_height)
result_screen = ResultScreen(screen_width, screen_height) result_screen = ResultScreen(screen_width, screen_height)
settings_screen = SettingsScreen(screen_width, screen_height)
screen_mapping = { screen_mapping = {
Screens.ENTRY: entry_screen, Screens.ENTRY: entry_screen,
Screens.TITLE: title_screen, Screens.TITLE: title_screen,
Screens.SONG_SELECT: song_select_screen, Screens.SONG_SELECT: song_select_screen,
Screens.GAME: game_screen, Screens.GAME: game_screen,
Screens.RESULT: result_screen Screens.RESULT: result_screen,
Screens.SETTINGS: settings_screen
} }
target = ray.load_render_texture(screen_width, screen_height) target = ray.load_render_texture(screen_width, screen_height)
ray.set_texture_filter(target.texture, ray.TextureFilter.TEXTURE_FILTER_TRILINEAR) ray.set_texture_filter(target.texture, ray.TextureFilter.TEXTURE_FILTER_TRILINEAR)
ray.gen_texture_mipmaps(target.texture) ray.gen_texture_mipmaps(target.texture)
#lmaooooooooooooo ray.rl_set_blend_factors_separate(RL_SRC_ALPHA, RL_ONE_MINUS_SRC_ALPHA, RL_ONE, RL_ONE_MINUS_SRC_ALPHA, RL_FUNC_ADD, RL_FUNC_ADD)
#rl_set_blend_factors_separate(RL_SRC_ALPHA, RL_ONE_MINUS_SRC_ALPHA, RL_ONE, RL_ONE_MINUS_SRC_ALPHA, RL_FUNC_ADD, RL_FUNC_ADD)
ray.rl_set_blend_factors_separate(0x302, 0x303, 1, 0x303, 0x8006, 0x8006)
ray.set_exit_key(ray.KeyboardKey.KEY_A) ray.set_exit_key(ray.KeyboardKey.KEY_A)
global_data.textures = load_all_textures_from_zip(Path('Graphics/lumendata/intermission.zip')) global_data.textures = load_all_textures_from_zip(Path('Graphics/lumendata/intermission.zip'))
while not ray.window_should_close(): while not ray.window_should_close():
@@ -118,7 +129,7 @@ def main():
ray.draw_texture_pro( ray.draw_texture_pro(
target.texture, target.texture,
ray.Rectangle(0, 0, target.texture.width, -target.texture.height), ray.Rectangle(0, 0, target.texture.width, -target.texture.height),
ray.Rectangle(0, 0, dpi_scale[0], dpi_scale[1]), ray.Rectangle(0, 0, screen_width, screen_height),
ray.Vector2(0,0), ray.Vector2(0,0),
0, 0,
ray.WHITE ray.WHITE

View File

@@ -1,12 +1,13 @@
[general] [general]
fps_counter = false fps_counter = false
judge_offset = 0 judge_offset = 0
autoplay = true visual_offset = 0
autoplay = false
sfx = true sfx = true
language = 'ja' language = "ja"
[paths] [paths]
tja_path = ['E:/Taiko/ESE', 'Songs', 'E:/Taiko/VersionSort'] tja_path = ['Songs']
video_path = 'Videos' video_path = 'Videos'
[keybinds] [keybinds]
@@ -16,9 +17,9 @@ right_don = ['J']
right_kat = ['K'] right_kat = ['K']
[audio] [audio]
device_type = 'ASIO' device_type = "Windows WASAPI"
buffer_size = 6 buffer_size = 22
sample_rate = 48000 sample_rate = 44100
exclusive = false exclusive = false
[video] [video]
@@ -26,4 +27,4 @@ screen_width = 1280
screen_height = 720 screen_height = 720
fullscreen = false fullscreen = false
borderless = false borderless = false
vsync = false vsync = true

View File

@@ -1,40 +1,48 @@
import json import json
import sys
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
import pandas as pd import pandas as pd
from libs.tja import TJAParser from libs.tja import TJAParser
from libs.utils import get_config from libs.utils import get_config
song_hashes: Optional[dict] = None song_hashes: Optional[dict] = None
def process_tja_file(tja_file): def process_tja_file(tja_file):
"""Process a single TJA file and return hash or None if error""" """Process a single TJA file and return hash or None if error"""
tja = TJAParser(tja_file) tja = TJAParser(tja_file)
all_notes = [] all_notes = []
for diff in tja.metadata.course_data: for diff in tja.metadata.course_data:
all_notes.extend(TJAParser.notes_to_position(TJAParser(tja.file_path), diff)) all_notes.extend(
TJAParser.notes_to_position(TJAParser(tja.file_path), diff)
)
hash = tja.hash_note_data(all_notes[0], all_notes[2]) hash = tja.hash_note_data(all_notes[0], all_notes[2])
return hash return hash
def build_song_hashes(output_file='cache/song_hashes.json'):
def build_song_hashes(output_file="cache/song_hashes.json"):
existing_hashes = {} existing_hashes = {}
output_path = Path(output_file) output_path = Path(output_file)
if output_path.exists(): if output_path.exists():
try: try:
with open(output_file, 'r', encoding='utf-8') as f: with open(output_file, "r", encoding="utf-8") as f:
existing_hashes = json.load(f) existing_hashes = json.load(f)
except (json.JSONDecodeError, IOError) as e: except (json.JSONDecodeError, IOError) as e:
print(f"Warning: Could not load existing hashes from {output_file}: {e}") print(
f"Warning: Could not load existing hashes from {output_file}: {e}"
)
existing_hashes = {} existing_hashes = {}
song_hashes = existing_hashes.copy() song_hashes = existing_hashes.copy()
tja_paths = get_config()['paths']['tja_path'] tja_paths = get_config()["paths"]["tja_path"]
all_tja_files = [] all_tja_files = []
for root_dir in tja_paths: for root_dir in tja_paths:
root_path = Path(root_dir) root_path = Path(root_dir)
all_tja_files.extend(root_path.rglob('*.tja')) all_tja_files.extend(root_path.rglob("*.tja"))
updated_count = 0 updated_count = 0
for tja_file in all_tja_files: for tja_file in all_tja_files:
@@ -45,14 +53,14 @@ def build_song_hashes(output_file='cache/song_hashes.json'):
existing_hash = None existing_hash = None
for h, data in song_hashes.items(): for h, data in song_hashes.items():
if data['file_path'] == str(tja_file): if data["file_path"] == str(tja_file):
existing_hash = h existing_hash = h
break break
if existing_hash is None: if existing_hash is None:
should_update = True should_update = True
else: else:
stored_modified = song_hashes[existing_hash].get('last_modified', 0) stored_modified = song_hashes[existing_hash].get("last_modified", 0)
if current_modified > stored_modified: if current_modified > stored_modified:
should_update = True should_update = True
del song_hashes[existing_hash] del song_hashes[existing_hash]
@@ -61,17 +69,19 @@ def build_song_hashes(output_file='cache/song_hashes.json'):
tja = TJAParser(tja_file) tja = TJAParser(tja_file)
all_notes = [] all_notes = []
for diff in tja.metadata.course_data: for diff in tja.metadata.course_data:
all_notes.extend(TJAParser.notes_to_position(TJAParser(tja.file_path), diff)) all_notes.extend(
TJAParser.notes_to_position(TJAParser(tja.file_path), diff)
)
hash_val = tja.hash_note_data(all_notes[0], all_notes[2]) hash_val = tja.hash_note_data(all_notes[0], all_notes[2])
song_hashes[hash_val] = { song_hashes[hash_val] = {
'file_path': str(tja_file), "file_path": str(tja_file),
'last_modified': current_modified, "last_modified": current_modified,
'title': tja.metadata.title, "title": tja.metadata.title,
'subtitle': tja.metadata.subtitle "subtitle": tja.metadata.subtitle,
} }
updated_count += 1 updated_count += 1
with open(output_file, 'w', encoding='utf-8') as f: with open(output_file, "w", encoding="utf-8") as f:
json.dump(song_hashes, f, indent=2, ensure_ascii=False) json.dump(song_hashes, f, indent=2, ensure_ascii=False)
print(f"Song hashes saved to {output_file}. Updated {updated_count} files.") print(f"Song hashes saved to {output_file}. Updated {updated_count} files.")
@@ -80,29 +90,84 @@ def build_song_hashes(output_file='cache/song_hashes.json'):
def get_japanese_songs_for_version(df, version_column): def get_japanese_songs_for_version(df, version_column):
# Filter rows where the specified version column has 'YES' # Filter rows where the specified version column has 'YES'
version_songs = df[df[version_column] == 'YES'] version_songs = df[df[version_column] != "NO"]
# Extract Japanese titles (JPTITLE column) # Extract Japanese titles (JPTITLE column)
japanese_titles = version_songs['TITLE 【TITLE2】\nJPTITLE「TITLE2」 より'].tolist() japanese_titles = version_songs[
"TITLE 【TITLE2】\nJPTITLE「TITLE2」 より"
].tolist()
japanese_titles = [name.split('\n') for name in japanese_titles] japanese_titles = [name.split("\n") for name in japanese_titles]
second_lines = [name[1] for name in japanese_titles if len(name) > 1] second_lines = [
name[1] if len(name) > 1 else name[0] for name in japanese_titles
]
all_tja_files = [] all_tja_files = []
direct_tja_paths = dict() direct_tja_paths = dict()
text_files = dict() text_files = dict()
tja_paths = get_config()['paths']['tja_path'] tja_paths = get_config()["paths"]["tja_path"]
for root_dir in tja_paths: for root_dir in tja_paths:
root_path = Path(root_dir) root_path = Path(root_dir)
all_tja_files.extend(root_path.rglob('*.tja')) all_tja_files.extend(root_path.rglob("*.tja"))
for tja in all_tja_files: for tja in all_tja_files:
tja_parse = TJAParser(tja) tja_parse = TJAParser(tja)
direct_tja_paths[tja_parse.metadata.title.get('ja', tja_parse.metadata.title['en'])] = tja tja_name = tja_parse.metadata.title.get(
"ja", tja_parse.metadata.title["en"]
)
if "【双打】" in tja_name:
tja_name = tja_name.strip("【双打】")
tja_name = tja_name.strip()
if tja_name in direct_tja_paths:
direct_tja_paths[tja_name].append(tja)
else:
direct_tja_paths[tja_name] = [tja]
for title in second_lines: for title in second_lines:
if "・・・" in title:
title = title.replace("・・・", "")
if "..." in title:
title = title.replace("・・・", "")
# Find all matching keys
matches = []
# Check for exact title match
if title in direct_tja_paths: if title in direct_tja_paths:
path = direct_tja_paths[title] for path in direct_tja_paths[title]:
elif title.split('')[0] in direct_tja_paths: matches.append((title, path))
path = direct_tja_paths[title.split('')[0]]
# Also check for partial matches with the first part before ''
title_prefix = title.split("")[0]
for key in direct_tja_paths:
if key.startswith(title_prefix) and key != title:
for path in direct_tja_paths[key]:
matches.append((key, path))
if not matches:
for key in direct_tja_paths:
if title.lower() in key.lower() or key.lower() in title.lower():
for path in direct_tja_paths[key]:
matches.append((key, path))
if not matches:
from difflib import get_close_matches
close_matches = get_close_matches(
title, direct_tja_paths.keys(), n=3, cutoff=0.6
)
for close_match in close_matches:
for path in direct_tja_paths[close_match]:
matches.append((close_match, path))
if len(matches) == 1:
path = matches[0][1]
elif len(matches) > 1:
print(
f"Multiple matches found for '{title.split('')[0]} ({title.split('')[1] if len(title.split('')) > 1 else ''})':"
)
for i, (key, path_val) in enumerate(matches, 1):
print(f"{i}. {key}: {path_val}")
choice = int(input("Choose number: ")) - 1
path = matches[choice][1]
else: else:
path = Path(input(f"NOT FOUND {title}: ")) path = Path(input(f"NOT FOUND {title}: "))
hash = process_tja_file(path) hash = process_tja_file(path)
@@ -110,15 +175,24 @@ def get_japanese_songs_for_version(df, version_column):
genre = Path(path).parent.parent.name genre = Path(path).parent.parent.name
if genre not in text_files: if genre not in text_files:
text_files[genre] = [] text_files[genre] = []
text_files[genre].append(f"{hash}|{tja_parse.metadata.title['en'].strip()}|{tja_parse.metadata.subtitle['en'].strip()}") text_files[genre].append(
f"{hash}|{tja_parse.metadata.title['en'].strip()}|{tja_parse.metadata.subtitle['en'].strip()}"
)
print(f"Added {title}: {path}")
for genre in text_files: for genre in text_files:
if not Path(version_column).exists(): if not Path(version_column).exists():
Path(version_column).mkdir() Path(version_column).mkdir()
if not Path(f"{version_column}/{genre}").exists(): if not Path(f"{version_column}/{genre}").exists():
Path(f"{version_column}/{genre}").mkdir() Path(f"{version_column}/{genre}").mkdir()
with open(Path(f"{version_column}/{genre}/song_list.txt"), 'w', encoding='utf-8-sig') as text_file: with open(
Path(f"{version_column}/{genre}/song_list.txt"),
"w",
encoding="utf-8-sig",
) as text_file:
for item in text_files[genre]: for item in text_files[genre]:
text_file.write(item + '\n') text_file.write(item + "\n")
return text_files return text_files
get_japanese_songs_for_version(pd.read_csv('full.csv'), 'AC12')
if len(sys.argv) > 1:
get_japanese_songs_for_version(pd.read_csv("full.csv"), sys.argv[1])

View File

@@ -1,12 +1,25 @@
import bisect
import hashlib import hashlib
import math import math
from collections import deque from collections import deque
from dataclasses import dataclass, field, fields from dataclasses import dataclass, field, fields
from functools import lru_cache
from pathlib import Path from pathlib import Path
from libs.utils import get_pixels_per_frame, strip_comments from libs.utils import get_pixels_per_frame, strip_comments
@lru_cache(maxsize=64)
def get_ms_per_measure(bpm_val, time_sig):
#https://gist.github.com/KatieFrogs/e000f406bbc70a12f3c34a07303eec8b#measure
if bpm_val == 0:
return 0
return 60000 * (time_sig * 4) / bpm_val
@lru_cache(maxsize=64)
def get_pixels_per_ms(pixels_per_frame):
return pixels_per_frame / (1000 / 60)
@dataclass() @dataclass()
class Note: class Note:
type: int = field(init=False) type: int = field(init=False)
@@ -150,6 +163,7 @@ def calculate_base_score(play_note_list: deque[Note | Drumroll | Balloon]) -> in
return math.ceil(total_score / 10) * 10 return math.ceil(total_score / 10) * 10
class TJAParser: class TJAParser:
DIFFS = {0: "easy", 1: "normal", 2: "hard", 3: "oni", 4: "edit", 5: "tower", 6: "dan"}
def __init__(self, path: Path, start_delay: int = 0, distance: int = 866): def __init__(self, path: Path, start_delay: int = 0, distance: int = 866):
self.file_path: Path = path self.file_path: Path = path
@@ -260,59 +274,54 @@ class TJAParser:
self.ex_data.limited_time = True self.ex_data.limited_time = True
def data_to_notes(self, diff): def data_to_notes(self, diff):
note_start = -1 diff_name = self.DIFFS.get(diff, "").lower()
note_end = -1
# Use enumerate for single iteration
note_start = note_end = -1
target_found = False target_found = False
diffs = {0: "easy", 1: "normal", 2: "hard", 3: "oni", 4: "edit", 5: "tower", 6: "dan"}
# Get the name corresponding to this difficulty number
diff_name = diffs.get(diff, "").lower()
i = 0 # Find the section boundaries
while i < len(self.data): for i, line in enumerate(self.data):
line = self.data[i]
# Check if this is the start of a difficulty section
if line.startswith("COURSE:"): if line.startswith("COURSE:"):
course_value = line[7:].strip().lower() course_value = line[7:].strip().lower()
target_found = (course_value.isdigit() and int(course_value) == diff) or course_value == diff_name
# Match either the exact number or the name elif target_found:
if (course_value.isdigit() and int(course_value) == diff) or course_value == diff_name: if note_start == -1 and line in ("#START", "#START P1"):
target_found = True
else:
target_found = False
# If we found our target section, look for START and END markers
if target_found:
if line == "#START":
note_start = i + 1 note_start = i + 1
elif line == "#END" and note_start != -1: elif line == "#END" and note_start != -1:
note_end = i note_end = i
break # We found our complete section break
i += 1 if note_start == -1 or note_end == -1:
return []
# Process the section with minimal string operations
notes = [] notes = []
bar = [] bar = []
#Check for measures and separate when comma exists section_data = self.data[note_start:note_end]
for i in range(note_start, note_end):
line = self.data[i] for line in section_data:
if line.startswith("#"): if line.startswith("#"):
bar.append(line) bar.append(line)
elif line == ',':
if not bar or all(item.startswith('#') for item in bar):
bar.append('')
notes.append(bar)
bar = []
else: else:
if line == ',': if line.endswith(','):
if len(bar) == 0 or all(item.startswith('#') for item in bar): bar.append(line[:-1])
bar.append('')
notes.append(bar) notes.append(bar)
bar = [] bar = []
else: else:
item = line.strip(',') bar.append(line)
bar.append(item)
if item != line: if bar: # Add remaining items
notes.append(bar) notes.append(bar)
bar = []
return notes return notes
def get_moji(self, play_note_list: deque[Note], ms_per_measure: float) -> None: def get_moji(self, play_note_list: list[Note], ms_per_measure: float) -> None:
se_notes = { se_notes = {
1: [0, 1, 2], # Note '1' has three possible sound effects 1: [0, 1, 2], # Note '1' has three possible sound effects
2: [3, 4], # Note '2' has two possible sound effects 2: [3, 4], # Note '2' has two possible sound effects
@@ -373,9 +382,9 @@ class TJAParser:
play_note_list[-3].moji = se_notes[play_note_list[-3].moji][2] play_note_list[-3].moji = se_notes[play_note_list[-3].moji][2]
def notes_to_position(self, diff: int): def notes_to_position(self, diff: int):
play_note_list: deque[Note | Drumroll | Balloon] = deque() play_note_list: list[Note | Drumroll | Balloon] = []
bar_list: deque[Note] = deque() draw_note_list: list[Note | Drumroll | Balloon] = []
draw_note_list: deque[Note | Drumroll | Balloon] = deque() bar_list: list[Note] = []
notes = self.data_to_notes(diff) notes = self.data_to_notes(diff)
balloon = self.metadata.course_data[diff].balloon.copy() balloon = self.metadata.course_data[diff].balloon.copy()
count = 0 count = 0
@@ -431,18 +440,14 @@ class TJAParser:
if skip_branch: if skip_branch:
continue continue
if bpm == 0: ms_per_measure = get_ms_per_measure(bpm, time_signature)
ms_per_measure = 0
else:
#https://gist.github.com/KatieFrogs/e000f406bbc70a12f3c34a07303eec8b#measure
ms_per_measure = 60000 * (time_signature*4) / bpm
#Create note object #Create note object
bar_line = Note() bar_line = Note()
#Determines how quickly the notes need to move across the screen to reach the judgment circle in time #Determines how quickly the notes need to move across the screen to reach the judgment circle in time
bar_line.pixels_per_frame = get_pixels_per_frame(bpm * time_signature * scroll_modifier, time_signature*4, self.distance) bar_line.pixels_per_frame = get_pixels_per_frame(bpm * time_signature * scroll_modifier, time_signature*4, self.distance)
pixels_per_ms = bar_line.pixels_per_frame / (1000 / 60) pixels_per_ms = get_pixels_per_ms(bar_line.pixels_per_frame)
bar_line.hit_ms = self.current_ms bar_line.hit_ms = self.current_ms
if pixels_per_ms == 0: if pixels_per_ms == 0:
@@ -455,7 +460,7 @@ class TJAParser:
if barline_added: if barline_added:
bar_line.display = False bar_line.display = False
bar_list.append(bar_line) bisect.insort(bar_list, bar_line, key=lambda x: x.load_ms)
barline_added = True barline_added = True
#Empty bar is still a bar, otherwise start increment #Empty bar is still a bar, otherwise start increment
@@ -471,12 +476,11 @@ class TJAParser:
continue continue
note = Note() note = Note()
note.hit_ms = self.current_ms note.hit_ms = self.current_ms
if pixels_per_ms == 0:
note.load_ms = note.hit_ms
else:
note.load_ms = note.hit_ms - (self.distance / pixels_per_ms)
note.type = int(item)
note.pixels_per_frame = bar_line.pixels_per_frame note.pixels_per_frame = bar_line.pixels_per_frame
pixels_per_ms = get_pixels_per_ms(note.pixels_per_frame)
note.load_ms = (note.hit_ms if pixels_per_ms == 0
else note.hit_ms - (self.distance / pixels_per_ms))
note.type = int(item)
note.index = index note.index = index
note.bpm = bpm note.bpm = bpm
note.gogo_time = gogo_time note.gogo_time = gogo_time
@@ -489,10 +493,7 @@ class TJAParser:
if balloon is None: if balloon is None:
raise Exception("Balloon note found, but no count was specified") raise Exception("Balloon note found, but no count was specified")
note = Balloon(note) note = Balloon(note)
if not balloon: note.count = 1 if not balloon else balloon.pop(0)
note.count = 1
else:
note.count = balloon.pop(0)
elif item == '8': elif item == '8':
new_pixels_per_ms = play_note_list[-1].pixels_per_frame / (1000 / 60) new_pixels_per_ms = play_note_list[-1].pixels_per_frame / (1000 / 60)
if new_pixels_per_ms == 0: if new_pixels_per_ms == 0:
@@ -502,6 +503,7 @@ class TJAParser:
note.pixels_per_frame = play_note_list[-1].pixels_per_frame note.pixels_per_frame = play_note_list[-1].pixels_per_frame
self.current_ms += increment self.current_ms += increment
play_note_list.append(note) play_note_list.append(note)
bisect.insort(draw_note_list, note, key=lambda x: x.load_ms)
self.get_moji(play_note_list, ms_per_measure) self.get_moji(play_note_list, ms_per_measure)
index += 1 index += 1
if len(play_note_list) > 3: if len(play_note_list) > 3:
@@ -513,9 +515,7 @@ class TJAParser:
# Sorting by load_ms is necessary for drawing, as some notes appear on the # Sorting by load_ms is necessary for drawing, as some notes appear on the
# screen slower regardless of when they reach the judge circle # screen slower regardless of when they reach the judge circle
# Bars can be sorted like this because they don't need hit detection # Bars can be sorted like this because they don't need hit detection
draw_note_list = deque(sorted(play_note_list, key=lambda n: n.load_ms)) return deque(play_note_list), deque(draw_note_list), deque(bar_list)
bar_list = deque(sorted(bar_list, key=lambda b: b.load_ms))
return play_note_list, draw_note_list, bar_list
def hash_note_data(self, play_notes: deque[Note | Drumroll | Balloon], bars: deque[Note]): def hash_note_data(self, play_notes: deque[Note | Drumroll | Balloon], bars: deque[Note]):
n = hashlib.sha256() n = hashlib.sha256()

View File

@@ -1,10 +1,12 @@
import hashlib import hashlib
import math
import os import os
import tempfile import tempfile
import time import time
import tomllib import tomlkit
import zipfile import zipfile
from dataclasses import dataclass, field from dataclasses import dataclass, field
from functools import lru_cache
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -85,6 +87,7 @@ def strip_comments(code: str) -> str:
index += 1 index += 1
return result return result
@lru_cache
def get_pixels_per_frame(bpm: float, time_signature: float, distance: float) -> float: def get_pixels_per_frame(bpm: float, time_signature: float, distance: float) -> float:
if bpm == 0: if bpm == 0:
return 0 return 0
@@ -94,10 +97,94 @@ def get_pixels_per_frame(bpm: float, time_signature: float, distance: float) ->
return (distance / total_frames) return (distance / total_frames)
def get_config() -> dict[str, Any]: def get_config() -> dict[str, Any]:
with open('config.toml', "rb") as f: with open('config.toml', "r", encoding="utf-8") as f:
config_file = tomllib.load(f) config_file = tomlkit.load(f)
return config_file return config_file
def save_config(config: dict[str, Any]) -> None:
with open('config.toml', "w", encoding="utf-8") as f:
tomlkit.dump(config, f)
def is_l_don_pressed() -> bool:
keys = get_config()["keybinds"]["left_don"]
for key in keys:
if ray.is_key_pressed(ord(key)):
return True
if ray.is_gamepad_available(0):
if ray.is_gamepad_button_pressed(0, 16):
return True
mid_x, mid_y = (1280//2, 720)
allowed_gestures = {ray.Gesture.GESTURE_TAP, ray.Gesture.GESTURE_DOUBLETAP}
if ray.get_gesture_detected() in allowed_gestures and ray.is_gesture_detected(ray.get_gesture_detected()):
for i in range(min(ray.get_touch_point_count(), 10)):
tap_pos = (ray.get_touch_position(i).x, ray.get_touch_position(i).y)
if math.dist(tap_pos, (mid_x, mid_y)) < 300 and tap_pos[0] <= mid_x:
return True
return False
def is_r_don_pressed() -> bool:
keys = get_config()["keybinds"]["right_don"]
for key in keys:
if ray.is_key_pressed(ord(key)):
return True
if ray.is_gamepad_available(0):
if ray.is_gamepad_button_pressed(0, 17):
return True
mid_x, mid_y = (1280//2, 720)
allowed_gestures = {ray.Gesture.GESTURE_TAP, ray.Gesture.GESTURE_DOUBLETAP}
if ray.get_gesture_detected() in allowed_gestures and ray.is_gesture_detected(ray.get_gesture_detected()):
for i in range(min(ray.get_touch_point_count(), 10)):
tap_pos = (ray.get_touch_position(i).x, ray.get_touch_position(i).y)
if math.dist(tap_pos, (mid_x, mid_y)) < 300 and tap_pos[0] > mid_x:
return True
return False
def is_l_kat_pressed() -> bool:
keys = get_config()["keybinds"]["left_kat"]
for key in keys:
if ray.is_key_pressed(ord(key)):
return True
if ray.is_gamepad_available(0):
if ray.is_gamepad_button_pressed(0, 10):
return True
mid_x, mid_y = (1280//2, 720)
allowed_gestures = {ray.Gesture.GESTURE_TAP, ray.Gesture.GESTURE_DOUBLETAP}
if ray.get_gesture_detected() in allowed_gestures and ray.is_gesture_detected(ray.get_gesture_detected()):
for i in range(min(ray.get_touch_point_count(), 10)):
tap_pos = (ray.get_touch_position(i).x, ray.get_touch_position(i).y)
if math.dist(tap_pos, (mid_x, mid_y)) >= 300 and tap_pos[0] <= mid_x:
return True
return False
def is_r_kat_pressed() -> bool:
keys = get_config()["keybinds"]["right_kat"]
for key in keys:
if ray.is_key_pressed(ord(key)):
return True
if ray.is_gamepad_available(0):
if ray.is_gamepad_button_pressed(0, 12):
return True
mid_x, mid_y = (1280//2, 720)
allowed_gestures = {ray.Gesture.GESTURE_TAP, ray.Gesture.GESTURE_DOUBLETAP}
if ray.get_gesture_detected() in allowed_gestures and ray.is_gesture_detected(ray.get_gesture_detected()):
for i in range(min(ray.get_touch_point_count(), 10)):
tap_pos = (ray.get_touch_position(i).x, ray.get_touch_position(i).y)
if math.dist(tap_pos, (mid_x, mid_y)) >= 300 and tap_pos[0] > mid_x:
return True
return False
def draw_scaled_texture(texture: ray.Texture, x: int, y: int, scale: float, color: ray.Color) -> None: def draw_scaled_texture(texture: ray.Texture, x: int, y: int, scale: float, color: ray.Color) -> None:
src_rect = ray.Rectangle(0, 0, texture.width, texture.height) src_rect = ray.Rectangle(0, 0, texture.width, texture.height)
dst_rect = ray.Rectangle(x, y, texture.width*scale, texture.height*scale) dst_rect = ray.Rectangle(x, y, texture.width*scale, texture.height*scale)

View File

@@ -8,11 +8,13 @@ dependencies = [
"numpy>=2.2.5", "numpy>=2.2.5",
"opencv-python>=4.11.0.86", "opencv-python>=4.11.0.86",
"pydub>=0.25.1", "pydub>=0.25.1",
"raylib>=5.5.0.2",
"raylib-dynamic>=5.5.0.2",
"ruff>=0.11.7", "ruff>=0.11.7",
"scipy>=1.15.2", "scipy>=1.15.2",
"sounddevice>=0.5.1", "sounddevice>=0.5.1",
"audioop-lts; python_version >= '3.13'", "audioop-lts; python_version >= '3.13'",
"moviepy>=2.1.2", "moviepy>=2.1.2",
"raylib-sdl>=5.5.0.2",
"soundfile>=0.13.1",
"pandas>=2.3.0",
"tomlkit>=0.13.3",
] ]

View File

@@ -2,7 +2,7 @@ from pathlib import Path
import pyray as ray import pyray as ray
from libs.utils import get_config, load_texture_from_zip from libs.utils import is_l_don_pressed, is_r_don_pressed, load_texture_from_zip
class EntryScreen: class EntryScreen:
@@ -18,16 +18,16 @@ class EntryScreen:
if not self.screen_init: if not self.screen_init:
self.screen_init = True self.screen_init = True
def on_screen_end(self): def on_screen_end(self, next_screen: str):
self.screen_init = False self.screen_init = False
return "SONG_SELECT" return next_screen
def update(self): def update(self):
self.on_screen_start() self.on_screen_start()
keys = get_config()["keybinds"]["left_don"] + get_config()["keybinds"]["right_don"] if is_l_don_pressed() or is_r_don_pressed():
for key in keys: return self.on_screen_end("SONG_SELECT")
if ray.is_key_pressed(ord(key)): if ray.is_key_pressed(ray.KeyboardKey.KEY_F1):
return self.on_screen_end() return self.on_screen_end("SETTINGS")
def draw(self): def draw(self):
ray.draw_texture(self.texture_footer, 0, self.height - 151, ray.WHITE) ray.draw_texture(self.texture_footer, 0, self.height - 151, ray.WHITE)

View File

@@ -15,6 +15,10 @@ from libs.utils import (
get_config, get_config,
get_current_ms, get_current_ms,
global_data, global_data,
is_l_don_pressed,
is_l_kat_pressed,
is_r_don_pressed,
is_r_kat_pressed,
load_all_textures_from_zip, load_all_textures_from_zip,
load_image_from_zip, load_image_from_zip,
load_texture_from_zip, load_texture_from_zip,
@@ -38,6 +42,7 @@ class GameScreen:
self.end_ms = 0 self.end_ms = 0
self.start_delay = 1000 self.start_delay = 1000
self.song_started = False self.song_started = False
self.prev_touch_count = 0
self.background = Background(width, height) self.background = Background(width, height)
@@ -150,14 +155,14 @@ class GameScreen:
result = cursor.fetchone() result = cursor.fetchone()
if result is None or session_data.result_score > result[0]: if result is None or session_data.result_score > result[0]:
insert_query = ''' insert_query = '''
INSERT OR REPLACE INTO Scores (hash, en_name, jp_name, diff, score, good, ok, bad, drumroll, combo) INSERT OR REPLACE INTO Scores (hash, en_name, jp_name, diff, score, good, ok, bad, drumroll, combo, clear)
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?); VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
''' '''
data = (hash, self.tja.metadata.title['en'], data = (hash, self.tja.metadata.title['en'],
self.tja.metadata.title['ja'], self.player_1.difficulty, self.tja.metadata.title.get('ja', ''), self.player_1.difficulty,
session_data.result_score, session_data.result_good, session_data.result_score, session_data.result_good,
session_data.result_ok, session_data.result_bad, session_data.result_ok, session_data.result_bad,
session_data.result_total_drumroll, session_data.result_max_combo) session_data.result_total_drumroll, session_data.result_max_combo, int(self.player_1.gauge.gauge_length > self.player_1.gauge.clear_start[min(self.player_1.difficulty, 3)]))
cursor.execute(insert_query, data) cursor.execute(insert_query, data)
con.commit() con.commit()
@@ -222,6 +227,7 @@ class Player:
self.player_number = player_number self.player_number = player_number
self.difficulty = difficulty self.difficulty = difficulty
self.visual_offset = get_config()["general"]["visual_offset"]
self.play_notes, self.draw_note_list, self.draw_bar_list = game_screen.tja.notes_to_position(self.difficulty) self.play_notes, self.draw_note_list, self.draw_bar_list = game_screen.tja.notes_to_position(self.difficulty)
self.total_notes = len([note for note in self.play_notes if 0 < note.type < 5]) self.total_notes = len([note for note in self.play_notes if 0 < note.type < 5])
@@ -259,7 +265,7 @@ class Player:
self.input_log: dict[float, tuple] = dict() self.input_log: dict[float, tuple] = dict()
self.gauge = Gauge(self.difficulty, game_screen.tja.metadata.course_data[self.difficulty].level) self.gauge = Gauge(self.difficulty, game_screen.tja.metadata.course_data[self.difficulty].level, self.total_notes)
self.gauge_hit_effect: list[GaugeHitEffect] = [] self.gauge_hit_effect: list[GaugeHitEffect] = []
self.autoplay_hit_side = 'L' self.autoplay_hit_side = 'L'
@@ -269,7 +275,7 @@ class Player:
return self.score, self.good_count, self.ok_count, self.bad_count, self.total_drumroll, self.max_combo return self.score, self.good_count, self.ok_count, self.bad_count, self.total_drumroll, self.max_combo
def get_position(self, game_screen: GameScreen, ms: float, pixels_per_frame: float) -> int: def get_position(self, game_screen: GameScreen, ms: float, pixels_per_frame: float) -> int:
return int(game_screen.width + pixels_per_frame * 60 / 1000 * (ms - game_screen.current_ms) - 64) return int(game_screen.width + pixels_per_frame * 60 / 1000 * (ms - game_screen.current_ms) - 64) - self.visual_offset
def animation_manager(self, animation_list: list): def animation_manager(self, animation_list: list):
if len(animation_list) <= 0: if len(animation_list) <= 0:
@@ -305,6 +311,7 @@ class Player:
if 0 < note.type <= 4: if 0 < note.type <= 4:
self.combo = 0 self.combo = 0
self.bad_count += 1 self.bad_count += 1
self.gauge.add_bad()
self.play_notes.popleft() self.play_notes.popleft()
elif note.type != 8: elif note.type != 8:
tail = self.play_notes[1] tail = self.play_notes[1]
@@ -437,6 +444,7 @@ class Player:
self.score += self.base_score self.score += self.base_score
self.base_score_list.append(ScoreCounterAnimation(self.base_score)) self.base_score_list.append(ScoreCounterAnimation(self.base_score))
self.note_correct(game_screen, curr_note) self.note_correct(game_screen, curr_note)
self.gauge.add_good()
elif (curr_note.hit_ms - Player.TIMING_OK) <= game_screen.current_ms <= (curr_note.hit_ms + Player.TIMING_OK): elif (curr_note.hit_ms - Player.TIMING_OK) <= game_screen.current_ms <= (curr_note.hit_ms + Player.TIMING_OK):
self.draw_judge_list.append(Judgement('OK', big, ms_display=game_screen.current_ms - curr_note.hit_ms)) self.draw_judge_list.append(Judgement('OK', big, ms_display=game_screen.current_ms - curr_note.hit_ms))
@@ -444,12 +452,14 @@ class Player:
self.score += 10 * math.floor(self.base_score / 2 / 10) self.score += 10 * math.floor(self.base_score / 2 / 10)
self.base_score_list.append(ScoreCounterAnimation(10 * math.floor(self.base_score / 2 / 10))) self.base_score_list.append(ScoreCounterAnimation(10 * math.floor(self.base_score / 2 / 10)))
self.note_correct(game_screen, curr_note) self.note_correct(game_screen, curr_note)
self.gauge.add_ok()
elif (curr_note.hit_ms - Player.TIMING_BAD) <= game_screen.current_ms <= (curr_note.hit_ms + Player.TIMING_BAD): elif (curr_note.hit_ms - Player.TIMING_BAD) <= game_screen.current_ms <= (curr_note.hit_ms + Player.TIMING_BAD):
self.draw_judge_list.append(Judgement('BAD', big, ms_display=game_screen.current_ms - curr_note.hit_ms)) self.draw_judge_list.append(Judgement('BAD', big, ms_display=game_screen.current_ms - curr_note.hit_ms))
self.bad_count += 1 self.bad_count += 1
self.combo = 0 self.combo = 0
self.play_notes.popleft() self.play_notes.popleft()
self.gauge.add_bad()
def drumroll_counter_manager(self, game_screen: GameScreen): def drumroll_counter_manager(self, game_screen: GameScreen):
if self.is_drumroll and self.curr_drumroll_count > 0 and self.drumroll_counter is None: if self.is_drumroll and self.curr_drumroll_count > 0 and self.drumroll_counter is None:
@@ -467,26 +477,23 @@ class Player:
if self.balloon_anim.is_finished: if self.balloon_anim.is_finished:
self.balloon_anim = None self.balloon_anim = None
def key_manager(self, game_screen: GameScreen): def handle_input(self, game_screen: GameScreen):
key_configs = [ input_checks = [
{"keys": get_config()["keybinds"]["left_don"], "type": "DON", "side": "L", "note_type": 1}, (is_l_don_pressed, 'DON', 'L', game_screen.sound_don),
{"keys": get_config()["keybinds"]["right_don"], "type": "DON", "side": "R", "note_type": 1}, (is_r_don_pressed, 'DON', 'R', game_screen.sound_don),
{"keys": get_config()["keybinds"]["left_kat"], "type": "KAT", "side": "L", "note_type": 2}, (is_l_kat_pressed, 'KAT', 'L', game_screen.sound_kat),
{"keys": get_config()["keybinds"]["right_kat"], "type": "KAT", "side": "R", "note_type": 2} (is_r_kat_pressed, 'KAT', 'R', game_screen.sound_kat)
] ]
for config in key_configs: for check_func, note_type, side, sound in input_checks:
for key in config["keys"]: if check_func():
if ray.is_key_pressed(ord(key)): self.lane_hit_effect = LaneHitEffect(note_type)
hit_type = config["type"] self.draw_drum_hit_list.append(DrumHitEffect(note_type, side))
self.lane_hit_effect = LaneHitEffect(hit_type)
self.draw_drum_hit_list.append(DrumHitEffect(hit_type, config["side"]))
sound = game_screen.sound_don if hit_type == "DON" else game_screen.sound_kat if get_config()["general"]["sfx"]:
if get_config()["general"]["sfx"]: audio.play_sound(sound)
audio.play_sound(sound)
self.check_note(game_screen, config["note_type"]) self.check_note(game_screen, 1 if note_type == 'DON' else 2)
self.input_log[game_screen.current_ms] = (hit_type, key) self.input_log[game_screen.current_ms] = (note_type, side)
def autoplay_manager(self, game_screen: GameScreen): def autoplay_manager(self, game_screen: GameScreen):
if not get_config()["general"]["autoplay"]: if not get_config()["general"]["autoplay"]:
@@ -558,7 +565,7 @@ class Player:
self.animation_manager(self.base_score_list) self.animation_manager(self.base_score_list)
self.score_counter.update(get_current_ms(), self.score) self.score_counter.update(get_current_ms(), self.score)
self.autoplay_manager(game_screen) self.autoplay_manager(game_screen)
self.key_manager(game_screen) self.handle_input(game_screen)
self.gauge.update(get_current_ms(), self.good_count, self.ok_count, self.bad_count, self.total_notes) self.gauge.update(get_current_ms(), self.good_count, self.ok_count, self.bad_count, self.total_notes)
@@ -686,6 +693,7 @@ class Player:
self.score_counter.draw(game_screen) self.score_counter.draw(game_screen)
for anim in self.base_score_list: for anim in self.base_score_list:
anim.draw(game_screen) anim.draw(game_screen)
#ray.draw_circle(game_screen.width//2, game_screen.height, 300, ray.ORANGE)
class Judgement: class Judgement:
def __init__(self, type: str, big: bool, ms_display: Optional[float]=None): def __init__(self, type: str, big: bool, ms_display: Optional[float]=None):
@@ -1232,8 +1240,10 @@ class ResultTransition:
x += texture_2.width x += texture_2.width
class Gauge: class Gauge:
def __init__(self, difficulty: int, level: int): def __init__(self, difficulty: int, level: int, total_notes: int):
self.gauge_length = 0 self.gauge_length = 0
self.previous_length = 0
self.total_notes = total_notes
self.difficulty = min(3, difficulty) self.difficulty = min(3, difficulty)
self.clear_start = [0, 0, 68, 68] self.clear_start = [0, 0, 68, 68]
self.level = min(10, level) self.level = min(10, level)
@@ -1285,16 +1295,29 @@ class Gauge:
self.rainbow_fade_in = None self.rainbow_fade_in = None
self.rainbow_animation = None self.rainbow_animation = None
def add_good(self):
self.gauge_update_anim = Animation.create_fade(450)
self.previous_length = int(self.gauge_length)
self.gauge_length += (1 / self.total_notes) * (100 * (self.clear_start[self.difficulty] / self.table[self.difficulty][self.level]["clear_rate"]))
if self.gauge_length > 87:
self.gauge_length = 87
def add_ok(self):
self.gauge_update_anim = Animation.create_fade(450)
self.previous_length = int(self.gauge_length)
self.gauge_length += ((1 * self.table[self.difficulty][self.level]["ok_multiplier"]) / self.total_notes) * (100 * (self.clear_start[self.difficulty] / self.table[self.difficulty][self.level]["clear_rate"]))
if self.gauge_length > 87:
self.gauge_length = 87
def add_bad(self):
self.previous_length = int(self.gauge_length)
self.gauge_length += ((1 * self.table[self.difficulty][self.level]["bad_multiplier"]) / self.total_notes) * (100 * (self.clear_start[self.difficulty] / self.table[self.difficulty][self.level]["clear_rate"]))
if self.gauge_length < 0:
self.gauge_length = 0
def update(self, current_ms: float, good_count: int, ok_count: int, bad_count: int, total_notes: int): def update(self, current_ms: float, good_count: int, ok_count: int, bad_count: int, total_notes: int):
gauge_length = int(((good_count +
(ok_count * self.table[self.difficulty][self.level]["ok_multiplier"] +
bad_count * self.table[self.difficulty][self.level]["bad_multiplier"])) / total_notes) * (100 * (self.clear_start[self.difficulty] / self.table[self.difficulty][self.level]["clear_rate"])))
previous_length = self.gauge_length
self.gauge_length = min(87, gauge_length)
if self.gauge_length == 87 and self.rainbow_fade_in is None: if self.gauge_length == 87 and self.rainbow_fade_in is None:
self.rainbow_fade_in = Animation.create_fade(450, initial_opacity=0.0, final_opacity=1.0) self.rainbow_fade_in = Animation.create_fade(450, initial_opacity=0.0, final_opacity=1.0)
if self.gauge_length > previous_length:
self.gauge_update_anim = Animation.create_fade(450)
if self.gauge_update_anim is not None: if self.gauge_update_anim is not None:
self.gauge_update_anim.update(current_ms) self.gauge_update_anim.update(current_ms)
@@ -1314,12 +1337,13 @@ class Gauge:
def draw(self, textures: list[ray.Texture]): def draw(self, textures: list[ray.Texture]):
ray.draw_texture(textures[0], 327, 132, ray.WHITE) ray.draw_texture(textures[0], 327, 132, ray.WHITE)
ray.draw_texture(textures[1], 483, 124, ray.WHITE) ray.draw_texture(textures[1], 483, 124, ray.WHITE)
if self.gauge_length == 87 and self.rainbow_fade_in is not None and self.rainbow_animation is not None: gauge_length = int(self.gauge_length)
if gauge_length == 87 and self.rainbow_fade_in is not None and self.rainbow_animation is not None:
if 0 < self.rainbow_animation.attribute < 8: if 0 < self.rainbow_animation.attribute < 8:
ray.draw_texture(textures[1 + int(self.rainbow_animation.attribute)], 483, 124, ray.fade(ray.WHITE, self.rainbow_fade_in.attribute)) ray.draw_texture(textures[1 + int(self.rainbow_animation.attribute)], 483, 124, ray.fade(ray.WHITE, self.rainbow_fade_in.attribute))
ray.draw_texture(textures[2 + int(self.rainbow_animation.attribute)], 483, 124, ray.fade(ray.WHITE, self.rainbow_fade_in.attribute)) ray.draw_texture(textures[2 + int(self.rainbow_animation.attribute)], 483, 124, ray.fade(ray.WHITE, self.rainbow_fade_in.attribute))
if self.rainbow_fade_in is None or not self.rainbow_fade_in.is_finished: if self.rainbow_fade_in is None or not self.rainbow_fade_in.is_finished:
for i in range(self.gauge_length): for i in range(gauge_length):
if i == 68: if i == 68:
ray.draw_texture(textures[16], 491 + (i*textures[13].width), 160 - 24, ray.WHITE) ray.draw_texture(textures[16], 491 + (i*textures[13].width), 160 - 24, ray.WHITE)
elif i > 68: elif i > 68:
@@ -1327,15 +1351,15 @@ class Gauge:
ray.draw_texture(textures[20], 491 + (i*textures[13].width) + 2, 160, ray.WHITE) ray.draw_texture(textures[20], 491 + (i*textures[13].width) + 2, 160, ray.WHITE)
else: else:
ray.draw_texture(textures[13], 491 + (i*textures[13].width), 160, ray.WHITE) ray.draw_texture(textures[13], 491 + (i*textures[13].width), 160, ray.WHITE)
if self.gauge_update_anim is not None and self.gauge_length < 88: if self.gauge_update_anim is not None and gauge_length < 88 and gauge_length != self.previous_length:
if self.gauge_length == 69: if gauge_length == 69:
ray.draw_texture(textures[17], 491 + (self.gauge_length*textures[13].width) - 13, 160 - 8 - 24, ray.fade(ray.WHITE, self.gauge_update_anim.attribute)) ray.draw_texture(textures[17], 491 + (gauge_length*textures[13].width) - 13, 160 - 8 - 24, ray.fade(ray.WHITE, self.gauge_update_anim.attribute))
elif self.gauge_length > 69: elif gauge_length > 69:
ray.draw_texture(textures[21], 491 + (self.gauge_length*textures[13].width) - 13, 160 - 8 - 22, ray.fade(ray.WHITE, self.gauge_update_anim.attribute)) ray.draw_texture(textures[21], 491 + (gauge_length*textures[13].width) - 13, 160 - 8 - 22, ray.fade(ray.WHITE, self.gauge_update_anim.attribute))
else: else:
ray.draw_texture(textures[14], 491 + (self.gauge_length*textures[13].width) - 13, 160 - 8, ray.fade(ray.WHITE, self.gauge_update_anim.attribute)) ray.draw_texture(textures[14], 491 + (gauge_length*textures[13].width) - 13, 160 - 8, ray.fade(ray.WHITE, self.gauge_update_anim.attribute))
ray.draw_texture(textures[10], 483, 124, ray.fade(ray.WHITE, 0.15)) ray.draw_texture(textures[10], 483, 124, ray.fade(ray.WHITE, 0.15))
if self.gauge_length >= 69: if gauge_length >= 69:
ray.draw_texture(textures[18], 1038, 141, ray.WHITE) ray.draw_texture(textures[18], 1038, 141, ray.WHITE)
ray.draw_texture(textures[19], 1187, 130, ray.WHITE) ray.draw_texture(textures[19], 1187, 130, ray.WHITE)
else: else:

View File

@@ -8,9 +8,10 @@ from libs.audio import audio
from libs.utils import ( from libs.utils import (
OutlinedText, OutlinedText,
draw_scaled_texture, draw_scaled_texture,
get_config,
get_current_ms, get_current_ms,
global_data, global_data,
is_l_don_pressed,
is_r_don_pressed,
load_all_textures_from_zip, load_all_textures_from_zip,
session_data, session_data,
) )
@@ -60,6 +61,7 @@ class ResultScreen:
['max_combo', session_data.result_max_combo]] ['max_combo', session_data.result_max_combo]]
self.update_index = 0 self.update_index = 0
self.is_skipped = False self.is_skipped = False
self.start_ms = get_current_ms()
def on_screen_end(self): def on_screen_end(self):
self.screen_init = False self.screen_init = False
@@ -94,6 +96,14 @@ class ResultScreen:
self.score_animator = ScoreAnimator(self.update_list[self.update_index][1]) self.score_animator = ScoreAnimator(self.update_list[self.update_index][1])
self.score_delay += 16.67 * 3 self.score_delay += 16.67 * 3
def handle_input(self):
if is_r_don_pressed() or is_l_don_pressed():
if not self.is_skipped:
self.is_skipped = True
else:
if self.fade_out is None:
self.fade_out = FadeOut()
audio.play_sound(self.sound_don)
def update(self): def update(self):
self.on_screen_start() self.on_screen_start()
@@ -108,16 +118,9 @@ class ResultScreen:
if self.gauge.is_finished and self.score_delay is None: if self.gauge.is_finished and self.score_delay is None:
self.score_delay = get_current_ms() + 1883 self.score_delay = get_current_ms() + 1883
left_dons = get_config()["keybinds"]["left_don"] if get_current_ms() >= self.start_ms + 5000:
right_dons = get_config()["keybinds"]["right_don"] self.handle_input()
for don in left_dons + right_dons:
if ray.is_key_pressed(ord(don)):
if not self.is_skipped:
self.is_skipped = True
else:
if self.fade_out is None:
self.fade_out = FadeOut()
audio.play_sound(self.sound_don)
self.update_score_animation(self.is_skipped) self.update_score_animation(self.is_skipped)
if self.fade_out is not None: if self.fade_out is not None:
@@ -263,12 +266,13 @@ class Gauge:
def draw(self, textures: list[ray.Texture]): def draw(self, textures: list[ray.Texture]):
color = ray.fade(ray.WHITE, self.gauge_fade_in.attribute) color = ray.fade(ray.WHITE, self.gauge_fade_in.attribute)
draw_scaled_texture(textures[217], 554, 109, (10/11), color) draw_scaled_texture(textures[217], 554, 109, (10/11), color)
if self.gauge_length == 87 and self.rainbow_animation is not None: gauge_length = int(self.gauge_length)
if gauge_length == 87 and self.rainbow_animation is not None:
if 0 < self.rainbow_animation.attribute < 8: if 0 < self.rainbow_animation.attribute < 8:
draw_scaled_texture(textures[217 + int(self.rainbow_animation.attribute)], 554, 109, (10/11), color) draw_scaled_texture(textures[217 + int(self.rainbow_animation.attribute)], 554, 109, (10/11), color)
draw_scaled_texture(textures[218 + int(self.rainbow_animation.attribute)], 554, 109, (10/11), color) draw_scaled_texture(textures[218 + int(self.rainbow_animation.attribute)], 554, 109, (10/11), color)
else: else:
for i in range(self.gauge_length+1): for i in range(gauge_length+1):
width = int(i * 7.2) width = int(i * 7.2)
if i == 69: if i == 69:
draw_scaled_texture(textures[192], 562 + width, 142 - 22, (10/11), color) draw_scaled_texture(textures[192], 562 + width, 142 - 22, (10/11), color)
@@ -285,7 +289,7 @@ class Gauge:
draw_scaled_texture(textures[226], 554, 109, (10/11), ray.fade(ray.WHITE, min(0.15, self.gauge_fade_in.attribute))) draw_scaled_texture(textures[226], 554, 109, (10/11), ray.fade(ray.WHITE, min(0.15, self.gauge_fade_in.attribute)))
draw_scaled_texture(textures[176], 1185, 116, (10/11), color) draw_scaled_texture(textures[176], 1185, 116, (10/11), color)
if self.gauge_length >= 69: if gauge_length >= 69:
draw_scaled_texture(textures[194], 1058, 124, (10/11), color) draw_scaled_texture(textures[194], 1058, 124, (10/11), color)
draw_scaled_texture(textures[195], 1182, 115, (10/11), color) draw_scaled_texture(textures[195], 1182, 115, (10/11), color)
else: else:

235
scenes/settings.py Normal file
View File

@@ -0,0 +1,235 @@
import pyray as ray
import sounddevice as sd
from libs.utils import (
get_config,
is_l_don_pressed,
is_l_kat_pressed,
is_r_don_pressed,
is_r_kat_pressed,
save_config,
)
class SettingsScreen:
def __init__(self, width: int, height: int):
self.width = width
self.height = height
self.screen_init = False
self.config = get_config()
self.headers = list(self.config.keys())
self.headers.append('Exit')
self.header_index = 0
self.setting_index = 0
self.in_setting_edit = False
self.editing_key = False
self.temp_key_input = ""
def on_screen_start(self):
if not self.screen_init:
self.screen_init = True
def on_screen_end(self):
self.screen_init = False
save_config(self.config)
return "ENTRY"
def get_current_settings(self):
"""Get the current section's settings as a list"""
current_header = self.headers[self.header_index]
if current_header == 'Exit' or current_header not in self.config:
return []
return list(self.config[current_header].items())
def handle_boolean_toggle(self, section, key):
"""Toggle boolean values"""
self.config[section][key] = not self.config[section][key]
def handle_numeric_change(self, section, key, increment):
"""Handle numeric value changes"""
current_value = self.config[section][key]
# Define step sizes for different settings
step_sizes = {
'judge_offset': 1,
'visual_offset': 1,
'buffer_size': 1,
'sample_rate': 1000,
}
step = step_sizes.get(key, 1)
new_value = current_value + (step * increment)
# Apply constraints for specific settings
if key == 'judge_offset':
new_value = max(-50, min(50, new_value))
elif key == 'visual_offset':
new_value = max(-20, min(20, new_value))
elif key == 'buffer_size':
new_value = max(1, min(32, new_value))
elif key == 'sample_rate':
valid_rates = [22050, 44100, 48000, 88200, 96000]
current_idx = valid_rates.index(current_value) if current_value in valid_rates else 2
new_idx = max(0, min(len(valid_rates) - 1, current_idx + increment))
new_value = valid_rates[new_idx]
self.config[section][key] = new_value
def handle_string_cycle(self, section, key):
"""Cycle through predefined string values"""
current_value = self.config[section][key]
hostapis = sd.query_hostapis()
audio_devices = []
if isinstance(hostapis, tuple):
for api in hostapis:
if isinstance(api, dict):
audio_devices.append(api['name'])
options = {
'language': ['ja', 'en'],
'device_type': audio_devices
}
if key in options:
values = options[key]
try:
current_idx = values.index(current_value)
new_idx = (current_idx + 1) % len(values)
self.config[section][key] = values[new_idx]
except ValueError:
self.config[section][key] = values[0]
def handle_key_binding(self, section, key):
"""Handle key binding changes"""
self.editing_key = True
self.temp_key_input = ""
def update_key_binding(self):
"""Update key binding based on input"""
key_pressed = ray.get_key_pressed()
if key_pressed != 0:
# Convert keycode to character
if 65 <= key_pressed <= 90: # A-Z
new_key = chr(key_pressed)
current_header = self.headers[self.header_index]
settings = self.get_current_settings()
if settings:
setting_key, _ = settings[self.setting_index]
self.config[current_header][setting_key] = [new_key]
self.editing_key = False
elif key_pressed == ray.KeyboardKey.KEY_ESCAPE:
self.editing_key = False
def update(self):
self.on_screen_start()
# Handle key binding editing
if self.editing_key:
self.update_key_binding()
return
current_header = self.headers[self.header_index]
# Exit handling
if current_header == 'Exit' and (is_l_don_pressed() or is_r_don_pressed()):
return self.on_screen_end()
# Navigation between sections
if not self.in_setting_edit:
if is_r_kat_pressed():
self.header_index = (self.header_index + 1) % len(self.headers)
self.setting_index = 0
elif is_l_kat_pressed():
self.header_index = (self.header_index - 1) % len(self.headers)
self.setting_index = 0
elif (is_l_don_pressed() or is_r_don_pressed()) and current_header != 'Exit':
self.in_setting_edit = True
else:
# Navigation within settings
settings = self.get_current_settings()
if not settings:
self.in_setting_edit = False
return
if is_r_kat_pressed():
self.setting_index = (self.setting_index + 1) % len(settings)
elif is_l_kat_pressed():
self.setting_index = (self.setting_index - 1) % len(settings)
elif is_r_don_pressed():
# Modify setting value
setting_key, setting_value = settings[self.setting_index]
if isinstance(setting_value, bool):
self.handle_boolean_toggle(current_header, setting_key)
elif isinstance(setting_value, (int, float)):
self.handle_numeric_change(current_header, setting_key, 1)
elif isinstance(setting_value, str):
if 'keybinds' in current_header:
self.handle_key_binding(current_header, setting_key)
else:
self.handle_string_cycle(current_header, setting_key)
elif isinstance(setting_value, list) and len(setting_value) > 0:
if isinstance(setting_value[0], str) and len(setting_value[0]) == 1:
# Key binding
self.handle_key_binding(current_header, setting_key)
elif is_l_don_pressed():
# Modify setting value (reverse direction for numeric)
setting_key, setting_value = settings[self.setting_index]
if isinstance(setting_value, bool):
self.handle_boolean_toggle(current_header, setting_key)
elif isinstance(setting_value, (int, float)):
self.handle_numeric_change(current_header, setting_key, -1)
elif isinstance(setting_value, str):
if 'keybinds' not in current_header:
self.handle_string_cycle(current_header, setting_key)
elif ray.is_key_pressed(ray.KeyboardKey.KEY_ESCAPE):
self.in_setting_edit = False
def draw(self):
# Draw title
ray.draw_text("SETTINGS", 20, 20, 30, ray.WHITE)
# Draw section headers
current_header = self.headers[self.header_index]
for i, key in enumerate(self.headers):
color = ray.GREEN
if key == current_header:
color = ray.YELLOW if not self.in_setting_edit else ray.ORANGE
ray.draw_text(f'{key}', 20, i*25 + 70, 20, color)
# Draw current section settings
if current_header != 'Exit' and current_header in self.config:
settings = self.get_current_settings()
# Draw settings list
for i, (key, value) in enumerate(settings):
color = ray.GREEN
if self.in_setting_edit and i == self.setting_index:
color = ray.YELLOW
# Format value display
if isinstance(value, list):
display_value = ', '.join(map(str, value))
else:
display_value = str(value)
ray.draw_text(f'{key}: {display_value}', 250, i*25 + 70, 20, color)
# Draw instructions
y_offset = len(settings) * 25 + 150
if not self.in_setting_edit:
ray.draw_text("Don/Kat: Navigate sections", 20, y_offset, 16, ray.GRAY)
ray.draw_text("L/R Don: Enter section", 20, y_offset + 20, 16, ray.GRAY)
else:
ray.draw_text("Don/Kat: Navigate settings", 20, y_offset, 16, ray.GRAY)
ray.draw_text("L/R Don: Modify value", 20, y_offset + 20, 16, ray.GRAY)
ray.draw_text("ESC: Back to sections", 20, y_offset + 40, 16, ray.GRAY)
if self.editing_key:
ray.draw_text("Press a key to bind (ESC to cancel)", 20, y_offset + 60, 16, ray.RED)
else:
# Draw exit instruction
ray.draw_text("Press Don to exit settings", 250, 100, 20, ray.GREEN)

File diff suppressed because it is too large Load Diff

View File

@@ -3,12 +3,13 @@ from pathlib import Path
import pyray as ray import pyray as ray
from libs import song_hash
from libs.animation import Animation from libs.animation import Animation
from libs.audio import audio from libs.audio import audio
from libs.utils import ( from libs.utils import (
get_config, get_config,
get_current_ms, get_current_ms,
is_l_don_pressed,
is_r_don_pressed,
load_all_textures_from_zip, load_all_textures_from_zip,
load_texture_from_zip, load_texture_from_zip,
) )
@@ -47,9 +48,6 @@ class TitleScreen:
if not self.screen_init: if not self.screen_init:
self.screen_init = True self.screen_init = True
self.load_textures() self.load_textures()
song_hash.song_hashes = song_hash.build_song_hashes()
self.scene = 'Opening Video' self.scene = 'Opening Video'
self.op_video = VideoPlayer(random.choice(self.op_video_list)) self.op_video = VideoPlayer(random.choice(self.op_video_list))
self.attract_video = VideoPlayer(random.choice(self.attract_video_list)) self.attract_video = VideoPlayer(random.choice(self.attract_video_list))
@@ -97,10 +95,8 @@ class TitleScreen:
self.on_screen_start() self.on_screen_start()
self.scene_manager() self.scene_manager()
keys = get_config()["keybinds"]["left_don"] + get_config()["keybinds"]["right_don"] if is_l_don_pressed() or is_r_don_pressed():
for key in keys: return self.on_screen_end()
if ray.is_key_pressed(ord(key)):
return self.on_screen_end()
def draw(self): def draw(self):
if self.scene == 'Opening Video': if self.scene == 'Opening Video':

170
uv.lock generated
View File

@@ -143,15 +143,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/c6/fa760e12a2483469e2bf5058c5faff664acf66cadb4df2ad6205b016a73d/imageio_ffmpeg-0.6.0-py3-none-win_amd64.whl", hash = "sha256:02fa47c83703c37df6bfe4896aab339013f62bf02c5ebf2dce6da56af04ffc0a", size = 31246824, upload-time = "2025-01-16T21:34:28.6Z" }, { url = "https://files.pythonhosted.org/packages/2c/c6/fa760e12a2483469e2bf5058c5faff664acf66cadb4df2ad6205b016a73d/imageio_ffmpeg-0.6.0-py3-none-win_amd64.whl", hash = "sha256:02fa47c83703c37df6bfe4896aab339013f62bf02c5ebf2dce6da56af04ffc0a", size = 31246824, upload-time = "2025-01-16T21:34:28.6Z" },
] ]
[[package]]
name = "inflection"
version = "0.5.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e1/7e/691d061b7329bc8d54edbf0ec22fbfb2afe61facb681f9aaa9bff7a27d04/inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417", size = 15091, upload-time = "2020-08-22T08:16:29.139Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/59/91/aa6bde563e0085a02a435aa99b49ef75b0a4b062635e606dab23ce18d720/inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2", size = 9454, upload-time = "2020-08-22T08:16:27.816Z" },
]
[[package]] [[package]]
name = "moviepy" name = "moviepy"
version = "2.1.2" version = "2.1.2"
@@ -235,6 +226,47 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a4/7d/f1c30a92854540bf789e9cd5dde7ef49bbe63f855b85a2e6b3db8135c591/opencv_python-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:085ad9b77c18853ea66283e98affefe2de8cc4c1f43eda4c100cf9b2721142ec", size = 39488044, upload-time = "2025-01-16T13:52:21.928Z" }, { url = "https://files.pythonhosted.org/packages/a4/7d/f1c30a92854540bf789e9cd5dde7ef49bbe63f855b85a2e6b3db8135c591/opencv_python-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:085ad9b77c18853ea66283e98affefe2de8cc4c1f43eda4c100cf9b2721142ec", size = 39488044, upload-time = "2025-01-16T13:52:21.928Z" },
] ]
[[package]]
name = "pandas"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
{ name = "python-dateutil" },
{ name = "pytz" },
{ name = "tzdata" },
]
sdist = { url = "https://files.pythonhosted.org/packages/72/51/48f713c4c728d7c55ef7444ba5ea027c26998d96d1a40953b346438602fc/pandas-2.3.0.tar.gz", hash = "sha256:34600ab34ebf1131a7613a260a61dbe8b62c188ec0ea4c296da7c9a06b004133", size = 4484490, upload-time = "2025-06-05T03:27:54.133Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/96/1e/ba313812a699fe37bf62e6194265a4621be11833f5fce46d9eae22acb5d7/pandas-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8adff9f138fc614347ff33812046787f7d43b3cef7c0f0171b3340cae333f6ca", size = 11551836, upload-time = "2025-06-05T03:26:22.784Z" },
{ url = "https://files.pythonhosted.org/packages/1b/cc/0af9c07f8d714ea563b12383a7e5bde9479cf32413ee2f346a9c5a801f22/pandas-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e5f08eb9a445d07720776df6e641975665c9ea12c9d8a331e0f6890f2dcd76ef", size = 10807977, upload-time = "2025-06-05T16:50:11.109Z" },
{ url = "https://files.pythonhosted.org/packages/ee/3e/8c0fb7e2cf4a55198466ced1ca6a9054ae3b7e7630df7757031df10001fd/pandas-2.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa35c266c8cd1a67d75971a1912b185b492d257092bdd2709bbdebe574ed228d", size = 11788230, upload-time = "2025-06-05T03:26:27.417Z" },
{ url = "https://files.pythonhosted.org/packages/14/22/b493ec614582307faf3f94989be0f7f0a71932ed6f56c9a80c0bb4a3b51e/pandas-2.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a0cc77b0f089d2d2ffe3007db58f170dae9b9f54e569b299db871a3ab5bf46", size = 12370423, upload-time = "2025-06-05T03:26:34.142Z" },
{ url = "https://files.pythonhosted.org/packages/9f/74/b012addb34cda5ce855218a37b258c4e056a0b9b334d116e518d72638737/pandas-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c06f6f144ad0a1bf84699aeea7eff6068ca5c63ceb404798198af7eb86082e33", size = 12990594, upload-time = "2025-06-06T00:00:13.934Z" },
{ url = "https://files.pythonhosted.org/packages/95/81/b310e60d033ab64b08e66c635b94076488f0b6ce6a674379dd5b224fc51c/pandas-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ed16339bc354a73e0a609df36d256672c7d296f3f767ac07257801aa064ff73c", size = 13745952, upload-time = "2025-06-05T03:26:39.475Z" },
{ url = "https://files.pythonhosted.org/packages/25/ac/f6ee5250a8881b55bd3aecde9b8cfddea2f2b43e3588bca68a4e9aaf46c8/pandas-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:fa07e138b3f6c04addfeaf56cc7fdb96c3b68a3fe5e5401251f231fce40a0d7a", size = 11094534, upload-time = "2025-06-05T03:26:43.23Z" },
{ url = "https://files.pythonhosted.org/packages/94/46/24192607058dd607dbfacdd060a2370f6afb19c2ccb617406469b9aeb8e7/pandas-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2eb4728a18dcd2908c7fccf74a982e241b467d178724545a48d0caf534b38ebf", size = 11573865, upload-time = "2025-06-05T03:26:46.774Z" },
{ url = "https://files.pythonhosted.org/packages/9f/cc/ae8ea3b800757a70c9fdccc68b67dc0280a6e814efcf74e4211fd5dea1ca/pandas-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9d8c3187be7479ea5c3d30c32a5d73d62a621166675063b2edd21bc47614027", size = 10702154, upload-time = "2025-06-05T16:50:14.439Z" },
{ url = "https://files.pythonhosted.org/packages/d8/ba/a7883d7aab3d24c6540a2768f679e7414582cc389876d469b40ec749d78b/pandas-2.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ff730713d4c4f2f1c860e36c005c7cefc1c7c80c21c0688fd605aa43c9fcf09", size = 11262180, upload-time = "2025-06-05T16:50:17.453Z" },
{ url = "https://files.pythonhosted.org/packages/01/a5/931fc3ad333d9d87b10107d948d757d67ebcfc33b1988d5faccc39c6845c/pandas-2.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba24af48643b12ffe49b27065d3babd52702d95ab70f50e1b34f71ca703e2c0d", size = 11991493, upload-time = "2025-06-05T03:26:51.813Z" },
{ url = "https://files.pythonhosted.org/packages/d7/bf/0213986830a92d44d55153c1d69b509431a972eb73f204242988c4e66e86/pandas-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:404d681c698e3c8a40a61d0cd9412cc7364ab9a9cc6e144ae2992e11a2e77a20", size = 12470733, upload-time = "2025-06-06T00:00:18.651Z" },
{ url = "https://files.pythonhosted.org/packages/a4/0e/21eb48a3a34a7d4bac982afc2c4eb5ab09f2d988bdf29d92ba9ae8e90a79/pandas-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6021910b086b3ca756755e86ddc64e0ddafd5e58e076c72cb1585162e5ad259b", size = 13212406, upload-time = "2025-06-05T03:26:55.992Z" },
{ url = "https://files.pythonhosted.org/packages/1f/d9/74017c4eec7a28892d8d6e31ae9de3baef71f5a5286e74e6b7aad7f8c837/pandas-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:094e271a15b579650ebf4c5155c05dcd2a14fd4fdd72cf4854b2f7ad31ea30be", size = 10976199, upload-time = "2025-06-05T03:26:59.594Z" },
{ url = "https://files.pythonhosted.org/packages/d3/57/5cb75a56a4842bbd0511c3d1c79186d8315b82dac802118322b2de1194fe/pandas-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c7e2fc25f89a49a11599ec1e76821322439d90820108309bf42130d2f36c983", size = 11518913, upload-time = "2025-06-05T03:27:02.757Z" },
{ url = "https://files.pythonhosted.org/packages/05/01/0c8785610e465e4948a01a059562176e4c8088aa257e2e074db868f86d4e/pandas-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c6da97aeb6a6d233fb6b17986234cc723b396b50a3c6804776351994f2a658fd", size = 10655249, upload-time = "2025-06-05T16:50:20.17Z" },
{ url = "https://files.pythonhosted.org/packages/e8/6a/47fd7517cd8abe72a58706aab2b99e9438360d36dcdb052cf917b7bf3bdc/pandas-2.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb32dc743b52467d488e7a7c8039b821da2826a9ba4f85b89ea95274f863280f", size = 11328359, upload-time = "2025-06-05T03:27:06.431Z" },
{ url = "https://files.pythonhosted.org/packages/2a/b3/463bfe819ed60fb7e7ddffb4ae2ee04b887b3444feee6c19437b8f834837/pandas-2.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:213cd63c43263dbb522c1f8a7c9d072e25900f6975596f883f4bebd77295d4f3", size = 12024789, upload-time = "2025-06-05T03:27:09.875Z" },
{ url = "https://files.pythonhosted.org/packages/04/0c/e0704ccdb0ac40aeb3434d1c641c43d05f75c92e67525df39575ace35468/pandas-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1d2b33e68d0ce64e26a4acc2e72d747292084f4e8db4c847c6f5f6cbe56ed6d8", size = 12480734, upload-time = "2025-06-06T00:00:22.246Z" },
{ url = "https://files.pythonhosted.org/packages/e9/df/815d6583967001153bb27f5cf075653d69d51ad887ebbf4cfe1173a1ac58/pandas-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:430a63bae10b5086995db1b02694996336e5a8ac9a96b4200572b413dfdfccb9", size = 13223381, upload-time = "2025-06-05T03:27:15.641Z" },
{ url = "https://files.pythonhosted.org/packages/79/88/ca5973ed07b7f484c493e941dbff990861ca55291ff7ac67c815ce347395/pandas-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:4930255e28ff5545e2ca404637bcc56f031893142773b3468dc021c6c32a1390", size = 10970135, upload-time = "2025-06-05T03:27:24.131Z" },
{ url = "https://files.pythonhosted.org/packages/24/fb/0994c14d1f7909ce83f0b1fb27958135513c4f3f2528bde216180aa73bfc/pandas-2.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f925f1ef673b4bd0271b1809b72b3270384f2b7d9d14a189b12b7fc02574d575", size = 12141356, upload-time = "2025-06-05T03:27:34.547Z" },
{ url = "https://files.pythonhosted.org/packages/9d/a2/9b903e5962134497ac4f8a96f862ee3081cb2506f69f8e4778ce3d9c9d82/pandas-2.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78ad363ddb873a631e92a3c063ade1ecfb34cae71e9a2be6ad100f875ac1042", size = 11474674, upload-time = "2025-06-05T03:27:39.448Z" },
{ url = "https://files.pythonhosted.org/packages/81/3a/3806d041bce032f8de44380f866059437fb79e36d6b22c82c187e65f765b/pandas-2.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:951805d146922aed8357e4cc5671b8b0b9be1027f0619cea132a9f3f65f2f09c", size = 11439876, upload-time = "2025-06-05T03:27:43.652Z" },
{ url = "https://files.pythonhosted.org/packages/15/aa/3fc3181d12b95da71f5c2537c3e3b3af6ab3a8c392ab41ebb766e0929bc6/pandas-2.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a881bc1309f3fce34696d07b00f13335c41f5f5a8770a33b09ebe23261cfc67", size = 11966182, upload-time = "2025-06-05T03:27:47.652Z" },
{ url = "https://files.pythonhosted.org/packages/37/e7/e12f2d9b0a2c4a2cc86e2aabff7ccfd24f03e597d770abfa2acd313ee46b/pandas-2.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e1991bbb96f4050b09b5f811253c4f3cf05ee89a589379aa36cd623f21a31d6f", size = 12547686, upload-time = "2025-06-06T00:00:26.142Z" },
{ url = "https://files.pythonhosted.org/packages/39/c2/646d2e93e0af70f4e5359d870a63584dacbc324b54d73e6b3267920ff117/pandas-2.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bb3be958022198531eb7ec2008cfc78c5b1eed51af8600c6c5d9160d89d8d249", size = 13231847, upload-time = "2025-06-05T03:27:51.465Z" },
]
[[package]] [[package]]
name = "pillow" name = "pillow"
version = "10.4.0" version = "10.4.0"
@@ -315,12 +347,14 @@ dependencies = [
{ name = "moviepy" }, { name = "moviepy" },
{ name = "numpy" }, { name = "numpy" },
{ name = "opencv-python" }, { name = "opencv-python" },
{ name = "pandas" },
{ name = "pydub" }, { name = "pydub" },
{ name = "raylib" }, { name = "raylib-sdl" },
{ name = "raylib-dynamic" },
{ name = "ruff" }, { name = "ruff" },
{ name = "scipy" }, { name = "scipy" },
{ name = "sounddevice" }, { name = "sounddevice" },
{ name = "soundfile" },
{ name = "tomlkit" },
] ]
[package.metadata] [package.metadata]
@@ -329,12 +363,26 @@ requires-dist = [
{ name = "moviepy", specifier = ">=2.1.2" }, { name = "moviepy", specifier = ">=2.1.2" },
{ name = "numpy", specifier = ">=2.2.5" }, { name = "numpy", specifier = ">=2.2.5" },
{ name = "opencv-python", specifier = ">=4.11.0.86" }, { name = "opencv-python", specifier = ">=4.11.0.86" },
{ name = "pandas", specifier = ">=2.3.0" },
{ name = "pydub", specifier = ">=0.25.1" }, { name = "pydub", specifier = ">=0.25.1" },
{ name = "raylib", specifier = ">=5.5.0.2" }, { name = "raylib-sdl", specifier = ">=5.5.0.2" },
{ name = "raylib-dynamic", specifier = ">=5.5.0.2" },
{ name = "ruff", specifier = ">=0.11.7" }, { name = "ruff", specifier = ">=0.11.7" },
{ name = "scipy", specifier = ">=1.15.2" }, { name = "scipy", specifier = ">=1.15.2" },
{ name = "sounddevice", specifier = ">=0.5.1" }, { name = "sounddevice", specifier = ">=0.5.1" },
{ name = "soundfile", specifier = ">=0.13.1" },
{ name = "tomlkit", specifier = ">=0.13.3" },
]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
] ]
[[package]] [[package]]
@@ -347,42 +395,40 @@ wheels = [
] ]
[[package]] [[package]]
name = "raylib" name = "pytz"
version = "5.5.0.2" version = "2025.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" }
{ name = "cffi" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8c/35/9bf3a2af73c55fd4310dcaec4f997c739888e0db9b4dfac71b7680810852/raylib-5.5.0.2.tar.gz", hash = "sha256:83c108ae3b4af40b53c93d1de2afbe309e986dd5efeb280ebe2e61c79956edb0", size = 181172, upload-time = "2024-11-26T11:12:02.791Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/9e/c4/ce21721b474eb8f65379f7315b382ccfe1d5df728eea4dcf287b874e7461/raylib-5.5.0.2-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:37eb0ec97fc6b08f989489a50e09b5dde519e1bb8eb17e4033ac82227b0e5eda", size = 1703742, upload-time = "2024-11-26T11:09:31.115Z" }, { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
{ url = "https://files.pythonhosted.org/packages/23/61/138e305c82549869bb8cd41abe75571559eafbeab6aed1ce7d8fbe3ffd58/raylib-5.5.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:bb9e506ecd3dbec6dba868eb036269837a8bde68220690842c3238239ee887ef", size = 1247449, upload-time = "2024-11-26T11:09:34.182Z" },
{ url = "https://files.pythonhosted.org/packages/85/e0/dc638c42d1a505f0992263d48e1434d82c21afdf376b06f549d2e281dfd4/raylib-5.5.0.2-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:70aa8bed67875a8cf25191f35263ef92d646bdfcb1f507915c81562a321f4931", size = 2184315, upload-time = "2024-11-26T11:09:36.715Z" },
{ url = "https://files.pythonhosted.org/packages/c9/1a/49db57283a28fdc1ff0e4604911b7fff085128c2ac8bdd9efa8c5c47439d/raylib-5.5.0.2-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:0365e8c578f72f598795d9377fc70342f0d62aa193c2f304ca048b3e28866752", size = 2278139, upload-time = "2024-11-26T11:09:39.475Z" },
{ url = "https://files.pythonhosted.org/packages/f0/8a/e1a690ab6889d4cb67346a2d32bad8b8e8b0f85ec826b00f76b0ad7e6ad6/raylib-5.5.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:5219be70e7fca03e9c4fddebf7e60e885d77137125c7a13f3800a947f8562a13", size = 1693944, upload-time = "2024-11-26T11:09:41.596Z" },
{ url = "https://files.pythonhosted.org/packages/69/2b/49bfa6833ad74ddf318d54ecafe73d535f583531469ecbd5b009d79667d1/raylib-5.5.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5233c529d9a0cfd469d88239c2182e55c5215a7755d83cc3d611148d3b9c9e67", size = 1706157, upload-time = "2024-11-26T11:09:43.6Z" },
{ url = "https://files.pythonhosted.org/packages/58/9c/8a3f4de0c81ad1228bf26410cfe3ecdc73011c59f18e542685ffc92c0120/raylib-5.5.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:1f76204ffbc492722b571b12dbdc0dca89b10da76ddf48c12a3968d2db061dff", size = 1248027, upload-time = "2025-01-04T20:21:46.269Z" },
{ url = "https://files.pythonhosted.org/packages/7f/16/63baf1aae94832b9f5d15cafcee67bb6dd07a20cf64d40bac09903b79274/raylib-5.5.0.2-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:f8cc2e39f1d6b29211a97ec0ac818a5b04c43a40e747e4b4622101d48c711f9e", size = 2195374, upload-time = "2024-11-26T11:09:46.114Z" },
{ url = "https://files.pythonhosted.org/packages/70/bd/61a006b4e3ce4a6ca974cb0ceeb19f3816815ebabac650e9bf82767e65f6/raylib-5.5.0.2-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:f12da578a28da7f48481f46323e5aab8dd25461982b0e80d045782d6e69649f5", size = 2299593, upload-time = "2024-11-26T11:09:48.963Z" },
{ url = "https://files.pythonhosted.org/packages/f4/4f/59d554cc495bea8235b17cebfc76ed57aaa602c613b870159e31282fd4c1/raylib-5.5.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:b40234bbad9523fd6a2049640c76a98b4d6f0b8f4bd19bd33eaee55faf5e050d", size = 1696780, upload-time = "2024-11-26T11:09:50.787Z" },
{ url = "https://files.pythonhosted.org/packages/ba/0a/78edc3ed1e2ca7e2ccea31ac5a8f4440a924662ee1042ecb76e08f465d8a/raylib-5.5.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2bfa9419a12eeb259f99c75c351db167968306295a5017cd4735241eaf2fa399", size = 1706104, upload-time = "2024-11-26T11:09:53.571Z" },
{ url = "https://files.pythonhosted.org/packages/48/fb/8b79a03c0d63bd6613d3e25df26d6c42fe36689f052abefa00a311be53fc/raylib-5.5.0.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:216456d0fa8da85c253a8596ca4b35c995bbf8af0fb8fa5244a4c6f5debfe294", size = 1248018, upload-time = "2024-11-26T11:09:56.257Z" },
{ url = "https://files.pythonhosted.org/packages/57/42/8ef2a8e21a2b4ba7ac4ea13b1dd9bc5d1084020ddca06ad0c6582c784a4e/raylib-5.5.0.2-cp313-cp313-manylinux2014_x86_64.whl", hash = "sha256:4f1c33b7d2404e70dbf3dd8d71701efb24902adfcb553122bf3d9441ea4fd6f0", size = 2299336, upload-time = "2024-11-26T11:09:59.307Z" },
{ url = "https://files.pythonhosted.org/packages/b4/67/4c25526fde4dabf5cc60093202f8d07a7dc4b1bd29d7b84db55b443a527c/raylib-5.5.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:6b60c97a9f6cfaeef4f59ff162ea29f02c18282d129edebb62f132b2ad6bb935", size = 1696790, upload-time = "2024-11-26T11:10:01.812Z" },
{ url = "https://files.pythonhosted.org/packages/4a/22/2e02e3738ad041f5ec2830aecdfab411fc2960bfc3400e03b477284bfaf7/raylib-5.5.0.2-pp311-pypy311_pp73-macosx_10_13_x86_64.whl", hash = "sha256:bc45fe1c0aac50aa319a9a66d44bb2bd0dcd038a44d95978191ae7bfeb4a06d8", size = 1216231, upload-time = "2025-02-12T04:21:59.38Z" },
{ url = "https://files.pythonhosted.org/packages/fe/7d/b29afedc4a706b12143f74f322cb32ad5a6f43e56aaca2a9fb89b0d94eee/raylib-5.5.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.whl", hash = "sha256:2242fd6079da5137e9863a447224f800adef6386ca8f59013a5d62cc5cadab2b", size = 1394928, upload-time = "2025-02-12T04:22:03.021Z" },
{ url = "https://files.pythonhosted.org/packages/b6/fa/2daf36d78078c6871b241168a36156169cfc8ea089faba5abe8edad304be/raylib-5.5.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e475a40764c9f83f9e66406bd86d85587eb923329a61ade463c3c59e1e880b16", size = 1564224, upload-time = "2025-02-12T04:22:05.911Z" },
] ]
[[package]] [[package]]
name = "raylib-dynamic" name = "raylib-sdl"
version = "5.5.0.2" version = "5.5.0.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "cffi" }, { name = "cffi" },
{ name = "inflection" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/43/74/ee15f73ad900bc0ea013852626998f78ba1eec020f470bf9a6873f3725b4/raylib_dynamic-5.5.0.2.tar.gz", hash = "sha256:125c95e51754c56fc045052b22642e2d53839cb1aac9db9df99355744daae868", size = 3172948, upload-time = "2024-11-26T11:12:04.91Z" } wheels = [
{ url = "https://files.pythonhosted.org/packages/32/b6/24c241f86ed6101f277d600c8f69cd12368848f1e94ed82d21b4d9dfba11/raylib_sdl-5.5.0.2-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:265a312ea73d8987a4c58075db7f27e356e217d52b22383ccfacf07f6eeb3556", size = 2287593, upload-time = "2024-11-26T11:11:13.524Z" },
{ url = "https://files.pythonhosted.org/packages/db/22/cada7093a7fc22ba8a68c032850175e7480b0e4149e782ee6093dca04006/raylib_sdl-5.5.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:8e86aa9ee0190cf02ae44bace9dc87379d171141a62daddd963dea40d40dbca7", size = 1745212, upload-time = "2025-01-04T20:21:04.281Z" },
{ url = "https://files.pythonhosted.org/packages/22/a2/387b20de1be19ece354a6c2cda30dc369948438dd824095b7da36ed974e7/raylib_sdl-5.5.0.2-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:3e2a91fef2525cc9a5a13fb3dbbd77e6c614b4d33f446c6047720516628b80d4", size = 2827982, upload-time = "2024-11-26T11:11:15.736Z" },
{ url = "https://files.pythonhosted.org/packages/1c/b4/f96cfb79981ac8e692975ae06ca224043d92b386b7f8a80cb515a85775c3/raylib_sdl-5.5.0.2-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:5263010953358beffabca8f7f1dfedcccc0f931afb91cfb242fd7783c875d12c", size = 2967405, upload-time = "2024-11-26T11:11:18.039Z" },
{ url = "https://files.pythonhosted.org/packages/5b/6c/c26425f317392a98c414c1f14974f511b1abf7f36d54069c80026ce505a3/raylib_sdl-5.5.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:4dcb099fa0bfa3048585d76445fe15cae3b2418c919f5777c54f429e1d3eb888", size = 1617306, upload-time = "2024-11-26T11:11:20.774Z" },
{ url = "https://files.pythonhosted.org/packages/f9/9d/8cd7466084f93b125c052f1825bc505d2822c19539dc5b3a2ac6a588fcaa/raylib_sdl-5.5.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:48e374f5c20e618f4c7c5664ac9e4f39f53245ffb9b62d6d133b8676716363b0", size = 2290643, upload-time = "2024-11-26T11:11:23.67Z" },
{ url = "https://files.pythonhosted.org/packages/cf/b0/1891f9712bed1083f1360d7f57f042a48bed110b5d5224256a33eeb9f302/raylib_sdl-5.5.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:6cd980a3fabdbe88a6449acf7ab24686854dce258dea9ff5b0eb46b7eae981c0", size = 1746303, upload-time = "2024-11-26T11:11:26.032Z" },
{ url = "https://files.pythonhosted.org/packages/24/de/6848b61e1df0eda838a61cb9e7060bd10d4af9616974c4360257c65ff8ff/raylib_sdl-5.5.0.2-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:a8e0fd9399188943c81dadf60d3d13198da1f9e0d84cc2aefa9e0c0d5c6266d6", size = 2840025, upload-time = "2024-11-26T11:11:29.705Z" },
{ url = "https://files.pythonhosted.org/packages/5e/b8/d41eacab8666fb955877cb5e3d4afea7ba4d01f668d9e91aa1716c1230cb/raylib_sdl-5.5.0.2-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:92407aad5382958ec684c35d3a28c8fc091faca3b58fc5dd8b3495a1065520ff", size = 2985567, upload-time = "2024-11-26T11:11:32.887Z" },
{ url = "https://files.pythonhosted.org/packages/00/07/d9f26daeda03ccd2d5b637fc3da25d039b39c27f1c2dde0ae9eeb2c40861/raylib_sdl-5.5.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:ab31941d2247a043beb545f34ebef1844f1d9c5fac749032e991ee8c18439c5a", size = 1619555, upload-time = "2024-11-26T11:11:35.387Z" },
{ url = "https://files.pythonhosted.org/packages/06/9c/e3d596db50bf4fb35f39478f1f529446dded5a7142c0583dbd8b19d52fe4/raylib_sdl-5.5.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bc6fe38110da1c98c341ede7064277ff79fb1b2a607787bd772974b4e4105d9a", size = 2290691, upload-time = "2024-11-26T11:11:38.045Z" },
{ url = "https://files.pythonhosted.org/packages/d9/d3/70d418057bb162d04df5cc5249ff83c38d02b55dbfe9363334518f2276b6/raylib_sdl-5.5.0.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:696ff487d3688a6c36c05c0cfcad1cd891351268739e5dfc9037692e958caa56", size = 1746153, upload-time = "2024-11-26T11:11:41.109Z" },
{ url = "https://files.pythonhosted.org/packages/56/8b/ccea24888331953eb43f2aa869d0a757093ebe88036d5234d1e83ef1785e/raylib_sdl-5.5.0.2-cp313-cp313-manylinux2014_x86_64.whl", hash = "sha256:10672ac9c3b3bac2da95d99b1e128b7ebded8aff4f3325063544ea55c0712a62", size = 2985152, upload-time = "2024-11-26T11:11:44.078Z" },
{ url = "https://files.pythonhosted.org/packages/15/d2/3a5e650d8c8dc3670a50ca000b6bed8f10f86872090eaa2a3450595f1528/raylib_sdl-5.5.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:29d7888a7ff42273a91b44520b209d24f20a9fd5eae0d9ac18851e52797c5d8b", size = 1619604, upload-time = "2024-11-26T11:11:46.22Z" },
{ url = "https://files.pythonhosted.org/packages/d9/2d/d531187e2c21d724fb07707dd71ac28d9e103d616ae595017f9b4679e8e2/raylib_sdl-5.5.0.2-pp311-pypy311_pp73-macosx_10_13_x86_64.whl", hash = "sha256:51017cb725fce7da0b8f220839f6db91e20bbb3ba58e7debe3b1fd67f4468188", size = 1809261, upload-time = "2025-02-12T04:22:11.465Z" },
{ url = "https://files.pythonhosted.org/packages/f0/c5/81ec5b8e0c9b89e95b85d20fd4685368dabc0d7fedc8050efdfe3bbcc2f2/raylib_sdl-5.5.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.whl", hash = "sha256:9453c29fc28b08dcae8a6432aeb07f13fbf8504fb1351941df64b9fb8ba0bac3", size = 2180109, upload-time = "2025-02-12T04:22:14.472Z" },
{ url = "https://files.pythonhosted.org/packages/b0/4f/8ba71611c74d6e3ff5e95d2935c1f5f98fc61183ebf70d4dfb09547a5767/raylib_sdl-5.5.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fd3841ab8d8d6ca24e4fbffc9514c58c0cf5bd29e3f2406006eba872416325b3", size = 1499920, upload-time = "2025-02-12T04:22:16.996Z" },
]
[[package]] [[package]]
name = "ruff" name = "ruff"
@@ -456,6 +502,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0a/c8/b3f566db71461cabd4b2d5b39bcc24a7e1c119535c8361f81426be39bb47/scipy-1.15.2-cp313-cp313t-win_amd64.whl", hash = "sha256:fe8a9eb875d430d81755472c5ba75e84acc980e4a8f6204d402849234d3017db", size = 40477705, upload-time = "2025-02-17T00:34:43.619Z" }, { url = "https://files.pythonhosted.org/packages/0a/c8/b3f566db71461cabd4b2d5b39bcc24a7e1c119535c8361f81426be39bb47/scipy-1.15.2-cp313-cp313t-win_amd64.whl", hash = "sha256:fe8a9eb875d430d81755472c5ba75e84acc980e4a8f6204d402849234d3017db", size = 40477705, upload-time = "2025-02-17T00:34:43.619Z" },
] ]
[[package]]
name = "six"
version = "1.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
]
[[package]] [[package]]
name = "sounddevice" name = "sounddevice"
version = "0.5.1" version = "0.5.1"
@@ -471,6 +526,34 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/af/9b/15217b04f3b36d30de55fef542389d722de63f1ad81f9c72d8afc98cb6ab/sounddevice-0.5.1-py3-none-win_amd64.whl", hash = "sha256:4313b63f2076552b23ac3e0abd3bcfc0c1c6a696fc356759a13bd113c9df90f1", size = 363634, upload-time = "2024-10-12T09:40:11.065Z" }, { url = "https://files.pythonhosted.org/packages/af/9b/15217b04f3b36d30de55fef542389d722de63f1ad81f9c72d8afc98cb6ab/sounddevice-0.5.1-py3-none-win_amd64.whl", hash = "sha256:4313b63f2076552b23ac3e0abd3bcfc0c1c6a696fc356759a13bd113c9df90f1", size = 363634, upload-time = "2024-10-12T09:40:11.065Z" },
] ]
[[package]]
name = "soundfile"
version = "0.13.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi" },
{ name = "numpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e1/41/9b873a8c055582859b239be17902a85339bec6a30ad162f98c9b0288a2cc/soundfile-0.13.1.tar.gz", hash = "sha256:b2c68dab1e30297317080a5b43df57e302584c49e2942defdde0acccc53f0e5b", size = 46156, upload-time = "2025-01-25T09:17:04.831Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/64/28/e2a36573ccbcf3d57c00626a21fe51989380636e821b341d36ccca0c1c3a/soundfile-0.13.1-py2.py3-none-any.whl", hash = "sha256:a23c717560da2cf4c7b5ae1142514e0fd82d6bbd9dfc93a50423447142f2c445", size = 25751, upload-time = "2025-01-25T09:16:44.235Z" },
{ url = "https://files.pythonhosted.org/packages/ea/ab/73e97a5b3cc46bba7ff8650a1504348fa1863a6f9d57d7001c6b67c5f20e/soundfile-0.13.1-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:82dc664d19831933fe59adad199bf3945ad06d84bc111a5b4c0d3089a5b9ec33", size = 1142250, upload-time = "2025-01-25T09:16:47.583Z" },
{ url = "https://files.pythonhosted.org/packages/a0/e5/58fd1a8d7b26fc113af244f966ee3aecf03cb9293cb935daaddc1e455e18/soundfile-0.13.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:743f12c12c4054921e15736c6be09ac26b3b3d603aef6fd69f9dde68748f2593", size = 1101406, upload-time = "2025-01-25T09:16:49.662Z" },
{ url = "https://files.pythonhosted.org/packages/58/ae/c0e4a53d77cf6e9a04179535766b3321b0b9ced5f70522e4caf9329f0046/soundfile-0.13.1-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:9c9e855f5a4d06ce4213f31918653ab7de0c5a8d8107cd2427e44b42df547deb", size = 1235729, upload-time = "2025-01-25T09:16:53.018Z" },
{ url = "https://files.pythonhosted.org/packages/57/5e/70bdd9579b35003a489fc850b5047beeda26328053ebadc1fb60f320f7db/soundfile-0.13.1-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:03267c4e493315294834a0870f31dbb3b28a95561b80b134f0bd3cf2d5f0e618", size = 1313646, upload-time = "2025-01-25T09:16:54.872Z" },
{ url = "https://files.pythonhosted.org/packages/fe/df/8c11dc4dfceda14e3003bb81a0d0edcaaf0796dd7b4f826ea3e532146bba/soundfile-0.13.1-py2.py3-none-win32.whl", hash = "sha256:c734564fab7c5ddf8e9be5bf70bab68042cd17e9c214c06e365e20d64f9a69d5", size = 899881, upload-time = "2025-01-25T09:16:56.663Z" },
{ url = "https://files.pythonhosted.org/packages/14/e9/6b761de83277f2f02ded7e7ea6f07828ec78e4b229b80e4ca55dd205b9dc/soundfile-0.13.1-py2.py3-none-win_amd64.whl", hash = "sha256:1e70a05a0626524a69e9f0f4dd2ec174b4e9567f4d8b6c11d38b5c289be36ee9", size = 1019162, upload-time = "2025-01-25T09:16:59.573Z" },
]
[[package]]
name = "tomlkit"
version = "0.13.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", size = 185207, upload-time = "2025-06-05T07:13:44.947Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901, upload-time = "2025-06-05T07:13:43.546Z" },
]
[[package]] [[package]]
name = "tqdm" name = "tqdm"
version = "4.67.1" version = "4.67.1"
@@ -482,3 +565,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
] ]
[[package]]
name = "tzdata"
version = "2025.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
]