From 759414e713bcdaa8b56625e3903cd0e8815f9c8d Mon Sep 17 00:00:00 2001 From: Yonokid <37304577+Yonokid@users.noreply.github.com> Date: Tue, 12 Aug 2025 01:10:19 -0400 Subject: [PATCH] fix audio engine, add difficulty sorting kind of --- libs/audio.py | 60 ++++++++++++++++++++++++++++--------------- libs/song_hash.py | 7 ----- libs/utils.py | 1 + scenes/devtest.py | 6 ++++- scenes/entry.py | 2 ++ scenes/game.py | 10 +++++--- scenes/loading.py | 8 ++---- scenes/result.py | 29 +++++++++++++++++++++ scenes/song_select.py | 35 ++++++++++++++++++------- 9 files changed, 111 insertions(+), 47 deletions(-) diff --git a/libs/audio.py b/libs/audio.py index 5a9fffd..30ebd81 100644 --- a/libs/audio.py +++ b/libs/audio.py @@ -6,9 +6,9 @@ from threading import Lock, Thread from typing import Optional import soundfile as sf -from numpy import abs as np_abs from numpy import ( arange, + clip, column_stack, float32, frombuffer, @@ -16,13 +16,12 @@ from numpy import ( int32, interp, mean, + multiply, ndarray, - ones, sqrt, uint8, zeros, ) -from numpy import max as np_max os.environ["SD_ENABLE_ASIO"] = "1" import sounddevice as sd @@ -514,7 +513,7 @@ class AudioEngine: self.running = False self.sound_queue: queue.Queue[str] = queue.Queue() self.music_queue = queue.Queue() - self.master_volume = 1.0 + self.master_volume = 0.70 self.output_channels = 2 # Default to stereo self.audio_device_ready = False @@ -523,6 +522,10 @@ class AudioEngine: self.update_thread_running = False self.type = type + self._output_buffer = None + self._channel_conversion_buffer = None + self._expected_frames = None + def _initialize_api(self) -> bool: """Set up API device""" # Find API and use its default device @@ -572,30 +575,25 @@ class AudioEngine: def _audio_callback(self, outdata: ndarray, frames: int, time: int, status: str) -> None: """callback function for the sounddevice stream""" + + if self._output_buffer is None: + raise Exception("output buffer was not allocated") if status: print(f"Status: {status}") self._process_sound_queue() self._process_music_queue() - # Pre-allocate output buffer (reuse if possible) - if not hasattr(self, '_output_buffer') or self._output_buffer.shape != (frames, self.output_channels): - self._output_buffer = zeros((frames, self.output_channels), dtype=float32) - else: - self._output_buffer.fill(0.0) # Clear previous data + self._output_buffer.fill(0.0) self._mix_sounds(self._output_buffer, frames) self._mix_music(self._output_buffer, frames) - # Apply master volume in-place if self.master_volume != 1.0: - self._output_buffer *= self.master_volume + multiply(self._output_buffer, self.master_volume, out=self._output_buffer) - # Apply limiter only if needed - max_val = np_max(np_abs(self._output_buffer)) - if max_val > 1.0: - self._output_buffer /= max_val + clip(self._output_buffer, -0.95, 0.95, out=self._output_buffer) outdata[:] = self._output_buffer @@ -668,19 +666,36 @@ class AudioEngine: output += music_data def _convert_channels(self, data: ndarray, input_channels: int) -> ndarray: - """channel conversion with caching""" + """Channel conversion using single pre-allocated buffer""" + if data.ndim == 1: + data = data.reshape(-1, 1) + input_channels = 1 + + frames = data.shape[0] + if input_channels == self.output_channels: return data + if self._channel_conversion_buffer is None: + raise Exception("channel conversion buffer was not allocated") + + self._channel_conversion_buffer[:frames, :self.output_channels].fill(0.0) + if input_channels == 1 and self.output_channels > 1: - return data[:, None] * ones((1, self.output_channels), dtype=float32) + # Mono to stereo/multi: broadcast to all channels + for ch in range(self.output_channels): + self._channel_conversion_buffer[:frames, ch] = data[:frames, 0] + elif input_channels > self.output_channels: if self.output_channels == 1: - return mean(data, axis=1, keepdims=True) + # Multi to mono: average channels + self._channel_conversion_buffer[:frames, 0] = mean(data[:frames, :input_channels], axis=1) else: - return data[:, :self.output_channels] + # Multi to fewer channels: take first N channels + self._channel_conversion_buffer[:frames, :self.output_channels] = data[:frames, :self.output_channels] - return data + # Return a view of the converted data + return self._channel_conversion_buffer[:frames, :self.output_channels] def _start_update_thread(self) -> None: """Start a thread to update music streams""" @@ -710,6 +725,9 @@ class AudioEngine: try: self._initialize_api() + self._expected_frames = self.buffer_size + self._output_buffer = zeros((self._expected_frames, self.output_channels), dtype=float32) + self._channel_conversion_buffer = zeros((self._expected_frames, max(8, self.output_channels)), dtype=float32) # Set up and start the stream extra_settings = None buffer_size = self.buffer_size @@ -752,6 +770,8 @@ class AudioEngine: self.music_streams = {} self.sound_queue = queue.Queue() self.music_queue = queue.Queue() + self._output_buffer = None + self._channel_conversion_buffer = None print("Audio device closed") return diff --git a/libs/song_hash.py b/libs/song_hash.py index 278ff8f..755c884 100644 --- a/libs/song_hash.py +++ b/libs/song_hash.py @@ -62,29 +62,22 @@ def build_song_hashes(output_dir=Path("cache")): files_to_process = [] - # O(n) pass to identify which files need processing for tja_path in all_tja_files: tja_path_str = str(tja_path) current_modified = tja_path.stat().st_mtime - # Skip files that haven't been modified since last run if current_modified <= saved_timestamp: - # File hasn't changed, just restore to global_data if we have it current_hash = path_to_hash.get(tja_path_str) if current_hash is not None: global_data.song_paths[tja_path] = current_hash continue - # O(1) lookup instead of nested loops current_hash = path_to_hash.get(tja_path_str) if current_hash is None: - # New file (modified after saved_timestamp) files_to_process.append(tja_path) else: - # File was modified after saved_timestamp, need to reprocess files_to_process.append(tja_path) - # Clean up old hash if current_hash in song_hashes: del song_hashes[current_hash] del path_to_hash[tja_path_str] diff --git a/libs/utils.py b/libs/utils.py index ba2425c..26bbc2b 100644 --- a/libs/utils.py +++ b/libs/utils.py @@ -208,6 +208,7 @@ class SessionData: result_max_combo: int = 0 result_total_drumroll: int = 0 result_gauge_length: int = 0 + prev_score: int = 0 session_data = SessionData() diff --git a/scenes/devtest.py b/scenes/devtest.py index 6684fb2..0ca4943 100644 --- a/scenes/devtest.py +++ b/scenes/devtest.py @@ -1,5 +1,7 @@ import pyray as ray +from libs.utils import session_data + class DevScreen: def __init__(self, width: int, height: int): @@ -13,12 +15,14 @@ class DevScreen: def on_screen_end(self, next_screen: str): self.screen_init = False + session_data.prev_score = 10000 + session_data.result_score = 100000 return next_screen def update(self): self.on_screen_start() if ray.is_key_pressed(ray.KeyboardKey.KEY_ENTER): - return self.on_screen_end('GAME') + return self.on_screen_end('RESULT') def draw(self): pass diff --git a/scenes/entry.py b/scenes/entry.py index c0a7087..c50f8e3 100644 --- a/scenes/entry.py +++ b/scenes/entry.py @@ -274,6 +274,8 @@ class Box: def _draw_text(self, color): text_x = self.x + (self.texture.width//2) - (self.text.texture.width//2) + if self.is_selected: + text_x += self.open.attribute text_y = self.y + 20 text_dest = ray.Rectangle(text_x, text_y, self.text.texture.width, self.text.texture.height) if self.is_selected: diff --git a/scenes/game.py b/scenes/game.py index 9bd3ff9..461efed 100644 --- a/scenes/game.py +++ b/scenes/game.py @@ -109,6 +109,10 @@ class GameScreen: cursor.execute(check_query, (hash,)) result = cursor.fetchone() if result is None or session_data.result_score > result[0]: + if result is None: + session_data.prev_score = 0 + else: + session_data.prev_score = result[0] insert_query = ''' INSERT OR REPLACE INTO Scores (hash, en_name, jp_name, diff, score, good, ok, bad, drumroll, combo, clear) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); @@ -542,11 +546,9 @@ class Player: tail = next((note for note in self.current_notes_draw[1:] if note.type == 8 and note.index > head.index), self.current_notes_draw[1]) is_big = int(head.type == 6) end_position = self.get_position_x(game_screen.width, game_screen.current_ms, tail.load_ms, tail.pixels_per_frame_x) - length = (end_position - start_position - 50) - if length <= 0: - end_position += 50 + length = end_position - start_position color = ray.Color(255, head.color, head.color, 255) - tex.draw_texture('notes', "8", frame=is_big, x=start_position+64, y=192, x2=length, color=color) + tex.draw_texture('notes', "8", frame=is_big, x=start_position+64, y=192, x2=length-64-32, color=color) tex.draw_texture('notes', "9", frame=is_big, x=end_position, y=192, color=color) tex.draw_texture('notes', str(head.type), frame=current_eighth % 2, x=start_position, y=192, color=color) diff --git a/scenes/loading.py b/scenes/loading.py index e271f59..17b8d86 100644 --- a/scenes/loading.py +++ b/scenes/loading.py @@ -42,12 +42,8 @@ class LoadScreen: def _load_navigator(self): """Background thread function to load navigator""" - try: - self.song_select_screen.load_navigator() - self.loading_complete = True - except Exception as e: - print(f"Error loading navigator: {e}") - self.loading_complete = True + self.song_select_screen.load_navigator() + self.loading_complete = True def on_screen_start(self): if not self.screen_init: diff --git a/scenes/result.py b/scenes/result.py index edce53c..5b03fef 100644 --- a/scenes/result.py +++ b/scenes/result.py @@ -1,4 +1,5 @@ from pathlib import Path +import sqlite3 import pyray as ray from raylib import SHADER_UNIFORM_FLOAT @@ -53,6 +54,7 @@ class ResultScreen: self.bottom_characters = BottomCharacters() self.crown = None self.state = None + self.high_score_indicator = None self.score_animator = ScoreAnimator(session_data.result_score) self.score = -1 self.good = -1 @@ -104,6 +106,9 @@ class ResultScreen: self.update_index += 1 self.score_animator = ScoreAnimator(self.update_list[self.update_index][1]) self.score_delay += 16.67 * 3 + if self.update_index > 0 and self.high_score_indicator is None: + if session_data.result_score > session_data.prev_score: + self.high_score_indicator = HighScoreIndicator(session_data.prev_score, session_data.result_score) def handle_input(self): if is_r_don_pressed() or is_l_don_pressed(): @@ -138,6 +143,9 @@ class ResultScreen: if self.gauge is not None: self.state = self.gauge.state + if self.high_score_indicator is not None: + self.high_score_indicator.update(get_current_ms()) + self.fade_in_bottom.update(get_current_ms()) alpha_loc = ray.get_shader_location(self.alpha_shader, "ext_alpha") alpha_value = ray.ffi.new('float*', self.fade_in_bottom.attribute) @@ -223,6 +231,9 @@ class ResultScreen: if self.crown is not None: self.crown.draw(self.crown_type) + if self.high_score_indicator is not None: + self.high_score_indicator.draw() + self.fade_in.draw() ray.draw_rectangle(0, 0, self.width, self.height, ray.fade(ray.BLACK, self.fade_out.attribute)) @@ -379,6 +390,24 @@ class ScoreAnimator: self.digit_index -= 1 return int(''.join([str(item[0]) for item in self.current_score_list])) +class HighScoreIndicator: + def __init__(self, old_score: int, new_score: int): + self.score_diff = new_score - old_score + self.move = tex.get_animation(18) + self.fade = tex.get_animation(19) + self.move.start() + self.fade.start() + + def update(self, current_ms): + self.move.update(current_ms) + self.fade.update(current_ms) + + def draw(self): + tex.draw_texture('score', 'high_score', y=self.move.attribute, fade=self.fade.attribute) + for i in range(len(str(self.score_diff))): + tex.draw_texture('score', 'high_score_num', x=-(i*14), frame=int(str(self.score_diff)[::-1][i]), y=self.move.attribute, fade=self.fade.attribute) + + class Gauge: def __init__(self, player_num: str, gauge_length: int): self.player_num = player_num diff --git a/scenes/song_select.py b/scenes/song_select.py index bc7281a..54e686f 100644 --- a/scenes/song_select.py +++ b/scenes/song_select.py @@ -334,7 +334,8 @@ class SongBox: 5: ray.Color(60, 104, 0, 255), 6: ray.Color(134, 88, 0, 255), 7: ray.Color(79, 40, 134, 255), - 8: ray.Color(148, 24, 0, 255) + 8: ray.Color(148, 24, 0, 255), + 14: ray.Color(157, 13, 31, 255) } def __init__(self, name: str, texture_index: int, is_dir: bool, tja: Optional[TJAParser] = None, tja_count: Optional[int] = None, box_texture: Optional[str] = None, name_texture_index: Optional[int] = None): @@ -460,7 +461,7 @@ class SongBox: def _draw_closed(self, x: int, y: int): tex.draw_texture('box', 'folder_texture_left', frame=self.texture_index, x=x) - offset = 1 if self.texture_index in {3, 9, 17} else 0 + offset = 1 if self.texture_index == 3 or self.texture_index >= 9 else 0 tex.draw_texture('box', 'folder_texture', frame=self.texture_index, x=x, x2=32, y=offset) tex.draw_texture('box', 'folder_texture_right', frame=self.texture_index, x=x) if self.texture_index == 9: @@ -476,8 +477,9 @@ class SongBox: if self.tja is not None and self.tja.ex_data.new: tex.draw_texture('yellow_box', 'ex_data_new_song_balloon', x=x, y=y) - if self.scores: - highest_key = max(self.scores.keys()) + valid_scores = {k: v for k, v in self.scores.items() if v is not None} + if valid_scores: + highest_key = max(valid_scores.keys()) score = self.scores[highest_key] if score and score[3] == 0: tex.draw_texture('yellow_box', 'crown_fc', x=x, y=y, frame=highest_key) @@ -503,7 +505,7 @@ class SongBox: self.hori_name.draw(self.hori_name.default_src, dest, ray.Vector2(0, 0), 0, color) tex.draw_texture('box', 'folder_texture_left', frame=self.texture_index, x=x - self.open_anim.attribute) - offset = 1 if self.texture_index in {3, 9, 17} else 0 + offset = 1 if self.texture_index == 3 or self.texture_index >= 9 else 0 tex.draw_texture('box', 'folder_texture', frame=self.texture_index, x=x - self.open_anim.attribute, y=offset, x2=324) tex.draw_texture('box', 'folder_texture_right', frame=self.texture_index, x=x + self.open_anim.attribute) @@ -788,6 +790,7 @@ class FileSystemItem: 'クラシック': 6, 'ゲームミュージック': 7, 'ナムコオリジナル': 8, + 'DIFFICULTY': 14 } """Base class for files and directories in the navigation system""" def __init__(self, path: Path, name: str): @@ -811,8 +814,9 @@ class Directory(FileSystemItem): self.collection = None if collection in Directory.COLLECTIONS: self.collection = collection - - if self.to_root or self.back: + if collection in FileSystemItem.GENRE_MAP: + texture_index = FileSystemItem.GENRE_MAP[collection] + elif self.to_root or self.back: texture_index = 17 self.box = SongBox(name, texture_index, True, tja_count=tja_count, box_texture=box_texture) @@ -1215,8 +1219,21 @@ class FileNavigator: # Add pre-generated content for this directory if dir_key in self.directory_contents: content_items = self.directory_contents[dir_key] - if isinstance(selected_item, Directory) and selected_item.collection == Directory.COLLECTIONS[0]: - content_items = self.new_items + if isinstance(selected_item, Directory): + if selected_item.collection == Directory.COLLECTIONS[0]: + content_items = self.new_items + elif selected_item.collection == Directory.COLLECTIONS[3]: + content_items = [] + parent_dir = selected_item.path.parent + for sibling_path in parent_dir.iterdir(): + if sibling_path.is_dir() and sibling_path != selected_item.path: + sibling_key = str(sibling_path) + if sibling_key in self.directory_contents: + for item in self.directory_contents[sibling_key]: + if isinstance(item, SongFile): + if 3 in item.tja.metadata.course_data and item.tja.metadata.course_data[3].level == 10: + item.box.texture_index = 14 + content_items.append(item) i = 1 for item in content_items: