fix audio engine, add difficulty sorting kind of

This commit is contained in:
Yonokid
2025-08-12 01:10:19 -04:00
parent f71499b0a8
commit 759414e713
9 changed files with 111 additions and 47 deletions

View File

@@ -6,9 +6,9 @@ from threading import Lock, Thread
from typing import Optional from typing import Optional
import soundfile as sf import soundfile as sf
from numpy import abs as np_abs
from numpy import ( from numpy import (
arange, arange,
clip,
column_stack, column_stack,
float32, float32,
frombuffer, frombuffer,
@@ -16,13 +16,12 @@ from numpy import (
int32, int32,
interp, interp,
mean, mean,
multiply,
ndarray, ndarray,
ones,
sqrt, sqrt,
uint8, uint8,
zeros, zeros,
) )
from numpy import max as np_max
os.environ["SD_ENABLE_ASIO"] = "1" os.environ["SD_ENABLE_ASIO"] = "1"
import sounddevice as sd import sounddevice as sd
@@ -514,7 +513,7 @@ class AudioEngine:
self.running = False self.running = False
self.sound_queue: queue.Queue[str] = queue.Queue() self.sound_queue: queue.Queue[str] = queue.Queue()
self.music_queue = 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.output_channels = 2 # Default to stereo
self.audio_device_ready = False self.audio_device_ready = False
@@ -523,6 +522,10 @@ class AudioEngine:
self.update_thread_running = False self.update_thread_running = False
self.type = type self.type = type
self._output_buffer = None
self._channel_conversion_buffer = None
self._expected_frames = None
def _initialize_api(self) -> bool: def _initialize_api(self) -> bool:
"""Set up API device""" """Set up API device"""
# Find API and use its default 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: def _audio_callback(self, outdata: ndarray, frames: int, time: int, status: str) -> None:
"""callback function for the sounddevice stream""" """callback function for the sounddevice stream"""
if self._output_buffer is None:
raise Exception("output buffer was not allocated")
if status: if status:
print(f"Status: {status}") print(f"Status: {status}")
self._process_sound_queue() self._process_sound_queue()
self._process_music_queue() self._process_music_queue()
# Pre-allocate output buffer (reuse if possible) self._output_buffer.fill(0.0)
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._mix_sounds(self._output_buffer, frames) self._mix_sounds(self._output_buffer, frames)
self._mix_music(self._output_buffer, frames) self._mix_music(self._output_buffer, frames)
# Apply master volume in-place
if self.master_volume != 1.0: 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 clip(self._output_buffer, -0.95, 0.95, out=self._output_buffer)
max_val = np_max(np_abs(self._output_buffer))
if max_val > 1.0:
self._output_buffer /= max_val
outdata[:] = self._output_buffer outdata[:] = self._output_buffer
@@ -668,19 +666,36 @@ class AudioEngine:
output += music_data output += music_data
def _convert_channels(self, data: ndarray, input_channels: int) -> ndarray: 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: if input_channels == self.output_channels:
return data 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: 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: elif input_channels > self.output_channels:
if self.output_channels == 1: 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: 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: def _start_update_thread(self) -> None:
"""Start a thread to update music streams""" """Start a thread to update music streams"""
@@ -710,6 +725,9 @@ class AudioEngine:
try: try:
self._initialize_api() 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 # Set up and start the stream
extra_settings = None extra_settings = None
buffer_size = self.buffer_size buffer_size = self.buffer_size
@@ -752,6 +770,8 @@ class AudioEngine:
self.music_streams = {} self.music_streams = {}
self.sound_queue = queue.Queue() self.sound_queue = queue.Queue()
self.music_queue = queue.Queue() self.music_queue = queue.Queue()
self._output_buffer = None
self._channel_conversion_buffer = None
print("Audio device closed") print("Audio device closed")
return return

View File

@@ -62,29 +62,22 @@ def build_song_hashes(output_dir=Path("cache")):
files_to_process = [] files_to_process = []
# O(n) pass to identify which files need processing
for tja_path in all_tja_files: for tja_path in all_tja_files:
tja_path_str = str(tja_path) tja_path_str = str(tja_path)
current_modified = tja_path.stat().st_mtime current_modified = tja_path.stat().st_mtime
# Skip files that haven't been modified since last run
if current_modified <= saved_timestamp: 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) current_hash = path_to_hash.get(tja_path_str)
if current_hash is not None: if current_hash is not None:
global_data.song_paths[tja_path] = current_hash global_data.song_paths[tja_path] = current_hash
continue continue
# O(1) lookup instead of nested loops
current_hash = path_to_hash.get(tja_path_str) current_hash = path_to_hash.get(tja_path_str)
if current_hash is None: if current_hash is None:
# New file (modified after saved_timestamp)
files_to_process.append(tja_path) files_to_process.append(tja_path)
else: else:
# File was modified after saved_timestamp, need to reprocess
files_to_process.append(tja_path) files_to_process.append(tja_path)
# Clean up old hash
if current_hash in song_hashes: if current_hash in song_hashes:
del song_hashes[current_hash] del song_hashes[current_hash]
del path_to_hash[tja_path_str] del path_to_hash[tja_path_str]

View File

@@ -208,6 +208,7 @@ class SessionData:
result_max_combo: int = 0 result_max_combo: int = 0
result_total_drumroll: int = 0 result_total_drumroll: int = 0
result_gauge_length: int = 0 result_gauge_length: int = 0
prev_score: int = 0
session_data = SessionData() session_data = SessionData()

View File

@@ -1,5 +1,7 @@
import pyray as ray import pyray as ray
from libs.utils import session_data
class DevScreen: class DevScreen:
def __init__(self, width: int, height: int): def __init__(self, width: int, height: int):
@@ -13,12 +15,14 @@ class DevScreen:
def on_screen_end(self, next_screen: str): def on_screen_end(self, next_screen: str):
self.screen_init = False self.screen_init = False
session_data.prev_score = 10000
session_data.result_score = 100000
return next_screen return next_screen
def update(self): def update(self):
self.on_screen_start() self.on_screen_start()
if ray.is_key_pressed(ray.KeyboardKey.KEY_ENTER): if ray.is_key_pressed(ray.KeyboardKey.KEY_ENTER):
return self.on_screen_end('GAME') return self.on_screen_end('RESULT')
def draw(self): def draw(self):
pass pass

View File

@@ -274,6 +274,8 @@ class Box:
def _draw_text(self, color): def _draw_text(self, color):
text_x = self.x + (self.texture.width//2) - (self.text.texture.width//2) 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_y = self.y + 20
text_dest = ray.Rectangle(text_x, text_y, self.text.texture.width, self.text.texture.height) text_dest = ray.Rectangle(text_x, text_y, self.text.texture.width, self.text.texture.height)
if self.is_selected: if self.is_selected:

View File

@@ -109,6 +109,10 @@ class GameScreen:
cursor.execute(check_query, (hash,)) cursor.execute(check_query, (hash,))
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]:
if result is None:
session_data.prev_score = 0
else:
session_data.prev_score = result[0]
insert_query = ''' insert_query = '''
INSERT OR REPLACE INTO Scores (hash, en_name, jp_name, diff, score, good, ok, bad, drumroll, combo, clear) INSERT OR REPLACE INTO Scores (hash, en_name, jp_name, diff, score, good, ok, bad, drumroll, combo, clear)
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); 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]) 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) 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) 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) length = end_position - start_position
if length <= 0:
end_position += 50
color = ray.Color(255, head.color, head.color, 255) 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', "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) tex.draw_texture('notes', str(head.type), frame=current_eighth % 2, x=start_position, y=192, color=color)

View File

@@ -42,12 +42,8 @@ class LoadScreen:
def _load_navigator(self): def _load_navigator(self):
"""Background thread function to load navigator""" """Background thread function to load navigator"""
try:
self.song_select_screen.load_navigator() self.song_select_screen.load_navigator()
self.loading_complete = True self.loading_complete = True
except Exception as e:
print(f"Error loading navigator: {e}")
self.loading_complete = True
def on_screen_start(self): def on_screen_start(self):
if not self.screen_init: if not self.screen_init:

View File

@@ -1,4 +1,5 @@
from pathlib import Path from pathlib import Path
import sqlite3
import pyray as ray import pyray as ray
from raylib import SHADER_UNIFORM_FLOAT from raylib import SHADER_UNIFORM_FLOAT
@@ -53,6 +54,7 @@ class ResultScreen:
self.bottom_characters = BottomCharacters() self.bottom_characters = BottomCharacters()
self.crown = None self.crown = None
self.state = None self.state = None
self.high_score_indicator = None
self.score_animator = ScoreAnimator(session_data.result_score) self.score_animator = ScoreAnimator(session_data.result_score)
self.score = -1 self.score = -1
self.good = -1 self.good = -1
@@ -104,6 +106,9 @@ class ResultScreen:
self.update_index += 1 self.update_index += 1
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
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): def handle_input(self):
if is_r_don_pressed() or is_l_don_pressed(): if is_r_don_pressed() or is_l_don_pressed():
@@ -138,6 +143,9 @@ class ResultScreen:
if self.gauge is not None: if self.gauge is not None:
self.state = self.gauge.state 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()) self.fade_in_bottom.update(get_current_ms())
alpha_loc = ray.get_shader_location(self.alpha_shader, "ext_alpha") alpha_loc = ray.get_shader_location(self.alpha_shader, "ext_alpha")
alpha_value = ray.ffi.new('float*', self.fade_in_bottom.attribute) alpha_value = ray.ffi.new('float*', self.fade_in_bottom.attribute)
@@ -223,6 +231,9 @@ class ResultScreen:
if self.crown is not None: if self.crown is not None:
self.crown.draw(self.crown_type) self.crown.draw(self.crown_type)
if self.high_score_indicator is not None:
self.high_score_indicator.draw()
self.fade_in.draw() self.fade_in.draw()
ray.draw_rectangle(0, 0, self.width, self.height, ray.fade(ray.BLACK, self.fade_out.attribute)) 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 self.digit_index -= 1
return int(''.join([str(item[0]) for item in self.current_score_list])) 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: class Gauge:
def __init__(self, player_num: str, gauge_length: int): def __init__(self, player_num: str, gauge_length: int):
self.player_num = player_num self.player_num = player_num

View File

@@ -334,7 +334,8 @@ class SongBox:
5: ray.Color(60, 104, 0, 255), 5: ray.Color(60, 104, 0, 255),
6: ray.Color(134, 88, 0, 255), 6: ray.Color(134, 88, 0, 255),
7: ray.Color(79, 40, 134, 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, 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): 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): def _draw_closed(self, x: int, y: int):
tex.draw_texture('box', 'folder_texture_left', frame=self.texture_index, x=x) 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', frame=self.texture_index, x=x, x2=32, y=offset)
tex.draw_texture('box', 'folder_texture_right', frame=self.texture_index, x=x) tex.draw_texture('box', 'folder_texture_right', frame=self.texture_index, x=x)
if self.texture_index == 9: if self.texture_index == 9:
@@ -476,8 +477,9 @@ class SongBox:
if self.tja is not None and self.tja.ex_data.new: 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) tex.draw_texture('yellow_box', 'ex_data_new_song_balloon', x=x, y=y)
if self.scores: valid_scores = {k: v for k, v in self.scores.items() if v is not None}
highest_key = max(self.scores.keys()) if valid_scores:
highest_key = max(valid_scores.keys())
score = self.scores[highest_key] score = self.scores[highest_key]
if score and score[3] == 0: if score and score[3] == 0:
tex.draw_texture('yellow_box', 'crown_fc', x=x, y=y, frame=highest_key) 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) 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) 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', 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) tex.draw_texture('box', 'folder_texture_right', frame=self.texture_index, x=x + self.open_anim.attribute)
@@ -788,6 +790,7 @@ class FileSystemItem:
'クラシック': 6, 'クラシック': 6,
'ゲームミュージック': 7, 'ゲームミュージック': 7,
'ナムコオリジナル': 8, 'ナムコオリジナル': 8,
'DIFFICULTY': 14
} }
"""Base class for files and directories in the navigation system""" """Base class for files and directories in the navigation system"""
def __init__(self, path: Path, name: str): def __init__(self, path: Path, name: str):
@@ -811,8 +814,9 @@ class Directory(FileSystemItem):
self.collection = None self.collection = None
if collection in Directory.COLLECTIONS: if collection in Directory.COLLECTIONS:
self.collection = collection self.collection = collection
if collection in FileSystemItem.GENRE_MAP:
if self.to_root or self.back: texture_index = FileSystemItem.GENRE_MAP[collection]
elif self.to_root or self.back:
texture_index = 17 texture_index = 17
self.box = SongBox(name, texture_index, True, tja_count=tja_count, box_texture=box_texture) 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 # Add pre-generated content for this directory
if dir_key in self.directory_contents: if dir_key in self.directory_contents:
content_items = self.directory_contents[dir_key] content_items = self.directory_contents[dir_key]
if isinstance(selected_item, Directory) and selected_item.collection == Directory.COLLECTIONS[0]: if isinstance(selected_item, Directory):
if selected_item.collection == Directory.COLLECTIONS[0]:
content_items = self.new_items 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 i = 1
for item in content_items: for item in content_items: