mirror of
https://github.com/Yonokid/PyTaiko.git
synced 2026-02-04 11:40:13 +01:00
fix audio engine, add difficulty sorting kind of
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user