diff --git a/PyTaiko.py b/PyTaiko.py index a3378cf..5692ced 100644 --- a/PyTaiko.py +++ b/PyTaiko.py @@ -297,7 +297,7 @@ def draw_fps(last_fps: int): elif last_fps < 60: pyray.draw_text_ex(global_data.font, f'{last_fps} FPS', (pos, pos), pos, 1, pyray.YELLOW) else: - pyray.draw_text_ex(global_data.font, f'{last_fps} FPS', (pos, pos), pos, 1, pyray.LIME) + pyray.draw_text_ex(pyray.get_font_default(), f'{last_fps} FPS', (pos, pos), pos, 1, pyray.LIME) def draw_outer_border(screen_width: int, screen_height: int, last_color: pyray.Color): pyray.draw_rectangle(-screen_width, 0, screen_width, screen_height, last_color) diff --git a/config.toml b/config.toml index 23284dc..e534732 100644 --- a/config.toml +++ b/config.toml @@ -64,7 +64,7 @@ device_type = 0 sample_rate = 44100 # buffer_size: Size in samples per audio buffer # - 0 = let driver choose (may result in very small buffers with ASIO, typically 64) -buffer_size = 32 +buffer_size = 128 [volume] sound = 1.0 diff --git a/libs/animation.py b/libs/animation.py index 2f59ce7..774fedc 100644 --- a/libs/animation.py +++ b/libs/animation.py @@ -84,6 +84,22 @@ class BaseAnimation(): self.restart() self.pause() + def copy(self): + """Create a copy of the animation with reset state.""" + new_anim = self.__class__.__new__(self.__class__) + new_anim.duration = self.duration + new_anim.delay = self.delay_saved + new_anim.delay_saved = self.delay_saved + new_anim.start_ms = get_current_ms() + new_anim.is_finished = False + new_anim.attribute = 0 + new_anim.is_started = False + new_anim.is_reversing = False + new_anim.unlocked = False + new_anim.loop = self.loop + new_anim.lock_input = self.lock_input + return new_anim + def _ease_in(self, progress: float, ease_type: str) -> float: if ease_type == "quadratic": return progress * progress @@ -133,6 +149,20 @@ class FadeAnimation(BaseAnimation): self.final_opacity = self.final_opacity_saved self.attribute = self.initial_opacity + def copy(self): + """Create a copy of the fade animation with reset state.""" + new_anim = super().copy() + new_anim.initial_opacity = self.initial_opacity_saved + new_anim.initial_opacity_saved = self.initial_opacity_saved + new_anim.final_opacity = self.final_opacity_saved + new_anim.final_opacity_saved = self.final_opacity_saved + new_anim.ease_in = self.ease_in + new_anim.ease_out = self.ease_out + new_anim.reverse_delay = self.reverse_delay_saved + new_anim.reverse_delay_saved = self.reverse_delay_saved + new_anim.attribute = self.initial_opacity_saved + return new_anim + def update(self, current_time_ms: float) -> None: if not self.is_started: return @@ -181,6 +211,20 @@ class MoveAnimation(BaseAnimation): self.start_position = self.start_position_saved self.attribute = self.start_position + def copy(self): + """Create a copy of the move animation with reset state.""" + new_anim = super().copy() + new_anim.reverse_delay = self.reverse_delay_saved + new_anim.reverse_delay_saved = self.reverse_delay_saved + new_anim.total_distance = self.total_distance_saved + new_anim.total_distance_saved = self.total_distance_saved + new_anim.start_position = self.start_position_saved + new_anim.start_position_saved = self.start_position_saved + new_anim.ease_in = self.ease_in + new_anim.ease_out = self.ease_out + new_anim.attribute = self.start_position_saved + return new_anim + def update(self, current_time_ms: float) -> None: if not self.is_started: return @@ -217,6 +261,13 @@ class TextureChangeAnimation(BaseAnimation): super().reset() self.attribute = self.textures[0][2] + def copy(self): + """Create a copy of the texture change animation with reset state.""" + new_anim = super().copy() + new_anim.textures = self.textures # List of tuples, can be shared + new_anim.attribute = self.textures[0][2] + return new_anim + def update(self, current_time_ms: float) -> None: if not self.is_started: return @@ -234,6 +285,10 @@ class TextureChangeAnimation(BaseAnimation): self.is_finished = True class TextStretchAnimation(BaseAnimation): + def copy(self): + """Create a copy of the text stretch animation with reset state.""" + return super().copy() + def update(self, current_time_ms: float) -> None: if not self.is_started: return @@ -275,6 +330,20 @@ class TextureResizeAnimation(BaseAnimation): self.initial_size = self.initial_size_saved self.final_size = self.final_size_saved + def copy(self): + """Create a copy of the texture resize animation with reset state.""" + new_anim = super().copy() + new_anim.initial_size = self.initial_size_saved + new_anim.initial_size_saved = self.initial_size_saved + new_anim.final_size = self.final_size_saved + new_anim.final_size_saved = self.final_size_saved + new_anim.reverse_delay = self.reverse_delay_saved + new_anim.reverse_delay_saved = self.reverse_delay_saved + new_anim.ease_in = self.ease_in + new_anim.ease_out = self.ease_out + new_anim.attribute = self.initial_size_saved + return new_anim + def update(self, current_time_ms: float) -> None: if not self.is_started: diff --git a/libs/audio.py b/libs/audio.py index ced6ef7..1544f66 100644 --- a/libs/audio.py +++ b/libs/audio.py @@ -92,6 +92,7 @@ ffi.cdef(""" void resume_music_stream(music music); void stop_music_stream(music music); void seek_music_stream(music music, float position); + bool music_stream_needs_update(music music); void update_music_stream(music music); bool is_music_stream_playing(music music); void set_music_volume(music music, float volume); @@ -359,11 +360,19 @@ class AudioEngine: else: logger.warning(f"Music stream {name} not found") - def update_music_stream(self, name: str) -> None: - """Update a music stream""" + def music_stream_needs_update(self, name: str) -> bool: + """Check if a music stream needs updating (buffers need refilling)""" if name in self.music_streams: music = self.music_streams[name] - lib.update_music_stream(music) # type: ignore + return lib.music_stream_needs_update(music) # type: ignore + return False + + def update_music_stream(self, name: str) -> None: + """Update a music stream (only if buffers need refilling)""" + if name in self.music_streams: + music = self.music_streams[name] + if lib.music_stream_needs_update(music): # type: ignore + lib.update_music_stream(music) # type: ignore else: logger.warning(f"Music stream {name} not found") diff --git a/libs/audio/audio.c b/libs/audio/audio.c index c85ce5c..5a9a426 100644 --- a/libs/audio/audio.c +++ b/libs/audio/audio.c @@ -175,6 +175,7 @@ void pause_music_stream(music music); void resume_music_stream(music music); void stop_music_stream(music music); void seek_music_stream(music music, float position); +bool music_stream_needs_update(music music); void update_music_stream(music music); bool is_music_stream_playing(music music); void set_music_volume(music music, float volume); @@ -1064,6 +1065,17 @@ void seek_music_stream(music music, float position) { pthread_mutex_unlock(&AUDIO.System.lock); } +bool music_stream_needs_update(music music) { + if (music.stream.buffer == NULL || music.ctxData == NULL) return false; + + pthread_mutex_lock(&AUDIO.System.lock); + bool needs_update = music.stream.buffer->isSubBufferProcessed[0] || + music.stream.buffer->isSubBufferProcessed[1]; + pthread_mutex_unlock(&AUDIO.System.lock); + + return needs_update; +} + void update_music_stream(music music) { if (music.stream.buffer == NULL || music.ctxData == NULL) return; @@ -1071,72 +1083,80 @@ void update_music_stream(music music) { SNDFILE *sndFile = ctx->snd_file; if (sndFile == NULL) return; + bool needs_refill[2]; + pthread_mutex_lock(&AUDIO.System.lock); + needs_refill[0] = music.stream.buffer->isSubBufferProcessed[0]; + needs_refill[1] = music.stream.buffer->isSubBufferProcessed[1]; + pthread_mutex_unlock(&AUDIO.System.lock); + + if (!needs_refill[0] && !needs_refill[1]) return; + + unsigned int subBufferSizeFrames = music.stream.buffer->sizeInFrames / 2; + float *buffer_data = (float *)music.stream.buffer->data; + bool needs_resampling = (ctx->resampler != NULL); + bool needs_mono_to_stereo = (music.stream.channels == 1 && AUDIO_DEVICE_CHANNELS == 2); + + unsigned int frames_to_read = subBufferSizeFrames; + if (needs_resampling) { + frames_to_read = (unsigned int)(subBufferSizeFrames / ctx->src_ratio) + 1; + } + + size_t required_size = frames_to_read * music.stream.channels * sizeof(float); + if (AUDIO.System.pcmBufferSize < required_size) { + FREE(AUDIO.System.pcmBuffer); + AUDIO.System.pcmBuffer = calloc(1, required_size); + AUDIO.System.pcmBufferSize = required_size; + } + for (int i = 0; i < 2; i++) { - pthread_mutex_lock(&AUDIO.System.lock); - bool needs_refill = music.stream.buffer->isSubBufferProcessed[i]; - pthread_mutex_unlock(&AUDIO.System.lock); + if (!needs_refill[i]) continue; - if (needs_refill) { - unsigned int subBufferSizeFrames = music.stream.buffer->sizeInFrames / 2; + sf_count_t frames_read = sf_readf_float(sndFile, (float*)AUDIO.System.pcmBuffer, frames_to_read); - unsigned int frames_to_read = subBufferSizeFrames; - if (ctx->resampler) { - frames_to_read = (unsigned int)(subBufferSizeFrames / ctx->src_ratio) + 1; + unsigned int subBufferOffset = i * subBufferSizeFrames * AUDIO_DEVICE_CHANNELS; + float *input_ptr = (float *)AUDIO.System.pcmBuffer; + sf_count_t frames_written = 0; + + if (needs_resampling) { + spx_uint32_t in_len = frames_read; + spx_uint32_t out_len = subBufferSizeFrames; + + int error = speex_resampler_process_interleaved_float( + ctx->resampler, + input_ptr, + &in_len, + buffer_data + subBufferOffset, + &out_len + ); + + if (error != RESAMPLER_ERR_SUCCESS) { + TRACELOG(LOG_WARNING, "Resampling failed with error: %d", error); } - if (AUDIO.System.pcmBufferSize < frames_to_read * music.stream.channels * sizeof(float)) { - FREE(AUDIO.System.pcmBuffer); - AUDIO.System.pcmBuffer = calloc(1, frames_to_read * music.stream.channels * sizeof(float)); - AUDIO.System.pcmBufferSize = frames_to_read * music.stream.channels * sizeof(float); - } - - sf_count_t frames_read = sf_readf_float(sndFile, (float*)AUDIO.System.pcmBuffer, frames_to_read); - - unsigned int subBufferOffset = i * subBufferSizeFrames * AUDIO_DEVICE_CHANNELS; - float *buffer_data = (float *)music.stream.buffer->data; - float *input_ptr = (float *)AUDIO.System.pcmBuffer; - sf_count_t frames_written = 0; - - if (ctx->resampler) { - spx_uint32_t in_len = frames_read; - spx_uint32_t out_len = subBufferSizeFrames; - - int error = speex_resampler_process_interleaved_float( - ctx->resampler, - input_ptr, - &in_len, - buffer_data + subBufferOffset, - &out_len - ); - - if (error != RESAMPLER_ERR_SUCCESS) { - TRACELOG(LOG_WARNING, "Resampling failed with error: %d", error); + frames_written = out_len; + } else { + if (needs_mono_to_stereo) { + for (int j = 0; j < frames_read; j++) { + buffer_data[subBufferOffset + j*2] = input_ptr[j]; + buffer_data[subBufferOffset + j*2 + 1] = input_ptr[j]; } - - frames_written = out_len; } else { - if (music.stream.channels == 1 && AUDIO_DEVICE_CHANNELS == 2) { - for (int j = 0; j < frames_read; j++) { - buffer_data[subBufferOffset + j*2] = input_ptr[j]; - buffer_data[subBufferOffset + j*2 + 1] = input_ptr[j]; - } - } else { - memcpy(buffer_data + subBufferOffset, input_ptr, frames_read * music.stream.channels * sizeof(float)); - } - frames_written = frames_read; + memcpy(buffer_data + subBufferOffset, input_ptr, frames_read * music.stream.channels * sizeof(float)); } + frames_written = frames_read; + } - if (frames_written < subBufferSizeFrames) { - unsigned int offset = subBufferOffset + (frames_written * AUDIO_DEVICE_CHANNELS); - unsigned int size = (subBufferSizeFrames - frames_written) * AUDIO_DEVICE_CHANNELS * sizeof(float); - memset(buffer_data + offset, 0, size); - } - - pthread_mutex_lock(&AUDIO.System.lock); - music.stream.buffer->isSubBufferProcessed[i] = false; - pthread_mutex_unlock(&AUDIO.System.lock); + if (frames_written < subBufferSizeFrames) { + unsigned int offset = subBufferOffset + (frames_written * AUDIO_DEVICE_CHANNELS); + unsigned int size = (subBufferSizeFrames - frames_written) * AUDIO_DEVICE_CHANNELS * sizeof(float); + memset(buffer_data + offset, 0, size); } } + + pthread_mutex_lock(&AUDIO.System.lock); + if (needs_refill[0]) music.stream.buffer->isSubBufferProcessed[0] = false; + if (needs_refill[1]) music.stream.buffer->isSubBufferProcessed[1] = false; + pthread_mutex_unlock(&AUDIO.System.lock); } bool is_music_stream_playing(music music) { diff --git a/libs/texture.py b/libs/texture.py index 9e40f81..63dc2f2 100644 --- a/libs/texture.py +++ b/libs/texture.py @@ -1,4 +1,3 @@ -import copy import json import logging import sys @@ -128,7 +127,7 @@ class TextureWrapper: if index not in self.animations: raise Exception(f"Unable to find id {index} in loaded animations") if is_copy: - new_anim = copy.deepcopy(self.animations[index]) + new_anim = self.animations[index].copy() if self.animations[index].loop: new_anim.start() return new_anim diff --git a/libs/utils.py b/libs/utils.py index e0cde8d..3baa83c 100644 --- a/libs/utils.py +++ b/libs/utils.py @@ -175,6 +175,12 @@ class OutlinedText: self.default_src = ray.Rectangle(0, 0, self.texture.width, self.texture.height) + self._last_outline_color = None + self._last_color = None + self._last_fade = None + self._outline_color_alloc = None + self._alpha_value = None + def _hash_text(self, text: str, font_size: int, color: ray.Color, vertical: bool): n = hashlib.sha256() n.update(text.encode('utf-8')) @@ -406,30 +412,37 @@ class OutlinedText: rotation (float): The rotation angle of the text. fade (float): The fade factor to apply to the text. """ - if isinstance(outline_color, tuple): - outline_color_alloc = ray.ffi.new("float[4]", [ - outline_color[0] / 255.0, - outline_color[1] / 255.0, - outline_color[2] / 255.0, - outline_color[3] / 255.0 - ]) - else: - outline_color_alloc = ray.ffi.new("float[4]", [ - outline_color.r / 255.0, - outline_color.g / 255.0, - outline_color.b / 255.0, - outline_color.a / 255.0 - ]) - ray.set_shader_value(self.shader, self.outline_color_loc, outline_color_alloc, SHADER_UNIFORM_VEC4) - if isinstance(color, tuple): - alpha_value = ray.ffi.new('float*', min(fade * 255, color[3]) / 255.0) - else: - alpha_value = ray.ffi.new('float*', min(fade * 255, color.a) / 255.0) + if self._last_outline_color != outline_color: + if isinstance(outline_color, tuple): + self._outline_color_alloc = ray.ffi.new("float[4]", [ + outline_color[0] / 255.0, + outline_color[1] / 255.0, + outline_color[2] / 255.0, + outline_color[3] / 255.0 + ]) + else: + self._outline_color_alloc = ray.ffi.new("float[4]", [ + outline_color.r / 255.0, + outline_color.g / 255.0, + outline_color.b / 255.0, + outline_color.a / 255.0 + ]) + ray.set_shader_value(self.shader, self.outline_color_loc, self._outline_color_alloc, SHADER_UNIFORM_VEC4) + self._last_outline_color = outline_color + + if self._last_color != color or self._last_fade != fade: + if isinstance(color, tuple): + self._alpha_value = ray.ffi.new('float*', min(fade * 255, color[3]) / 255.0) + else: + self._alpha_value = ray.ffi.new('float*', min(fade * 255, color.a) / 255.0) + ray.set_shader_value(self.shader, self.alpha_loc, self._alpha_value, SHADER_UNIFORM_FLOAT) + self._last_color = color + self._last_fade = fade + if fade != 1.1: final_color = ray.fade(color, fade) else: final_color = color - ray.set_shader_value(self.shader, self.alpha_loc, alpha_value, SHADER_UNIFORM_FLOAT) if not self.vertical: offset = (10 * global_tex.screen_scale)-10 else: diff --git a/scenes/game.py b/scenes/game.py index d0b4131..ff6909d 100644 --- a/scenes/game.py +++ b/scenes/game.py @@ -321,10 +321,10 @@ class GameScreen(Screen): def draw(self): if self.movie is not None: self.movie.draw() - elif self.background is not None: - self.background.draw() + #elif self.background is not None: + #self.background.draw() self.player_1.draw(self.current_ms, self.start_ms, self.mask_shader) - self.draw_overlay() + #self.draw_overlay() class Player: TIMING_GOOD = 25.0250015258789 @@ -861,7 +861,8 @@ class Player: if background is not None: background.add_renda() self.score += 100 - self.base_score_list.append(ScoreCounterAnimation(self.player_num, 100, self.is_2p)) + if len(self.base_score_list) < 5: + self.base_score_list.append(ScoreCounterAnimation(self.player_num, 100, self.is_2p)) if not self.current_notes_draw: return if not isinstance(self.current_notes_draw[0], Drumroll): @@ -880,7 +881,8 @@ class Player: self.curr_balloon_count += 1 self.total_drumroll += 1 self.score += 100 - self.base_score_list.append(ScoreCounterAnimation(self.player_num, 100, self.is_2p)) + if len(self.base_score_list) < 5: + self.base_score_list.append(ScoreCounterAnimation(self.player_num, 100, self.is_2p)) if self.curr_balloon_count == note.count: self.is_balloon = False note.popped = True @@ -954,11 +956,13 @@ class Player: big = curr_note.type == NoteType.DON_L or curr_note.type == NoteType.KAT_L if (curr_note.hit_ms - good_window_ms) <= ms_from_start <= (curr_note.hit_ms + good_window_ms): - self.draw_judge_list.append(Judgment(Judgments.GOOD, big, self.is_2p)) + if len(self.draw_judge_list) < 7: + self.draw_judge_list.append(Judgment(Judgments.GOOD, big, self.is_2p)) self.lane_hit_effect = LaneHitEffect(drum_type, Judgments.GOOD, self.is_2p) self.good_count += 1 self.score += self.base_score - self.base_score_list.append(ScoreCounterAnimation(self.player_num, self.base_score, self.is_2p)) + if len(self.base_score_list) < 5: + self.base_score_list.append(ScoreCounterAnimation(self.player_num, self.base_score, self.is_2p)) self.input_log[curr_note.index] = 'GOOD' self.note_correct(curr_note, current_time) if self.gauge is not None: @@ -976,7 +980,8 @@ class Player: self.lane_hit_effect = LaneHitEffect(drum_type, Judgments.OK, self.is_2p) self.ok_count += 1 self.score += 10 * math.floor(self.base_score / 2 / 10) - self.base_score_list.append(ScoreCounterAnimation(self.player_num, 10 * math.floor(self.base_score / 2 / 10), self.is_2p)) + if len(self.base_score_list) < 5: + self.base_score_list.append(ScoreCounterAnimation(self.player_num, 10 * math.floor(self.base_score / 2 / 10), self.is_2p)) self.input_log[curr_note.index] = 'OK' self.note_correct(curr_note, current_time) if self.gauge is not None: @@ -1038,7 +1043,8 @@ class Player: def spawn_hit_effects(self, drum_type: DrumType, side: Side): self.lane_hit_effect = LaneHitEffect(drum_type, Judgments.BAD, self.is_2p) # Bad code detected... - self.draw_drum_hit_list.append(DrumHitEffect(drum_type, side, self.is_2p)) + if len(self.draw_drum_hit_list) < 4: + self.draw_drum_hit_list.append(DrumHitEffect(drum_type, side, self.is_2p)) def handle_input(self, ms_from_start: float, current_time: float, background: Optional[Background]): input_checks = [ @@ -1153,7 +1159,7 @@ class Player: finished_arcs = [] for i, anim in enumerate(self.draw_arc_list): anim.update(current_time) - if anim.is_finished: + if anim.is_finished and len(self.gauge_hit_effect) < 7: self.gauge_hit_effect.append(GaugeHitEffect(anim.note_type, anim.is_big, self.is_2p)) finished_arcs.append(i) for i in reversed(finished_arcs): @@ -1409,7 +1415,7 @@ class Player: if dan_transition is not None: dan_transition.draw() - self.draw_overlays(mask_shader) + #self.draw_overlays(mask_shader) class Judgment: """Shows the judgment of the player's hit""" diff --git a/scenes/practice/game.py b/scenes/practice/game.py index cf43cb8..2fb8eff 100644 --- a/scenes/practice/game.py +++ b/scenes/practice/game.py @@ -32,6 +32,7 @@ from scenes.game import ( DrumType, GameScreen, JudgeCounter, + Judgments, LaneHitEffect, Player, Side, @@ -311,7 +312,7 @@ class PracticePlayer(Player): self.check_note(ms_from_start, drum_type, current_time, background) def spawn_hit_effects(self, drum_type: DrumType, side: Side): - self.lane_hit_effect = LaneHitEffect(drum_type, self.is_2p) + self.lane_hit_effect = LaneHitEffect(drum_type, Judgments.BAD, self.is_2p) self.draw_drum_hit_list.append(PracticeDrumHitEffect(drum_type, side, self.is_2p, player_num=self.player_num)) def draw_overlays(self, mask_shader: ray.Shader):