import pygame import os from tja_parser import TJA, Course, Note class EditorState: def __init__(self): self.tja = None self.current_course_name = 'Oni' self.current_time = 0.0 # ms self.is_playing = False self.zoom_x = 0.2 # pixels per ms self.scroll_y = 0 self.selected_notes = [] # List of Note objects self.file_path = None self.audio_path = None self.snap_grid = 4 # 1/4 beat (16th note) def load_file(self, path): if path.lower().endswith('.tja'): self.file_path = path self.tja = TJA(path) # Find audio wave = self.tja.headers.get('WAVE', '') base_dir = os.path.dirname(path) self.audio_path = os.path.join(base_dir, wave) elif path.lower().endswith('.ogg'): self.audio_path = path # Check for TJA base_name = os.path.splitext(path)[0] tja_path = base_name + '.tja' if os.path.exists(tja_path): self.file_path = tja_path self.tja = TJA(tja_path) else: self.tja = TJA() self.tja.headers['WAVE'] = os.path.basename(path) self.tja.headers['TITLE'] = os.path.basename(base_name) self.load_audio() def load_audio(self): if self.audio_path and os.path.exists(self.audio_path): try: pygame.mixer.music.load(self.audio_path) except Exception as e: print(f"Failed to load audio: {e}") def get_current_course(self): if not self.tja: return None if self.current_course_name not in self.tja.courses: self.tja.courses[self.current_course_name] = Course(self.current_course_name) return self.tja.courses[self.current_course_name] def toggle_play(self): if self.is_playing: pygame.mixer.music.pause() self.is_playing = False else: # Sync pygame music to current time try: # pygame.mixer.music.play(start=self.current_time / 1000.0) # Note: 'start' in play() is usually working, but set_pos might be needed depending on implementation if self.current_time < 0: # Start from 0 if negative (pre-song offset) pygame.mixer.music.play(start=0) else: pygame.mixer.music.play(start=self.current_time / 1000.0) self.is_playing = True except: pass def update(self, dt): if self.is_playing: self.current_time += dt # Optional: Sync with mixer position to avoid drift # mixer_pos = pygame.mixer.music.get_pos() # Returns ms played since start of 'play' # This is tricky because get_pos resets on play(). # Simple dt addition is often smoother for short edits. def save(self): if self.tja and self.file_path: self.tja.save(self.file_path) elif self.tja and self.audio_path: # Save as .tja next to ogg base = os.path.splitext(self.audio_path)[0] self.file_path = base + ".tja" self.tja.save(self.file_path) def record_note(self, note_type): """Adds a note at current_time, snapped to grid""" course = self.get_current_course() if not course: return # Calculate snapped time bpm = self.tja.headers.get('BPM', 120) beat_ms = 60000 / bpm snap_ms = beat_ms / self.snap_grid # e.g. 1/4 beat = 16th note # Quantize current time raw_time = self.current_time snapped_time = round(raw_time / snap_ms) * snap_ms # Calculate Measure/Beat beat_total = snapped_time / beat_ms measure = int(beat_total / 4) # Assuming 4/4 beat = beat_total % 4 # Avoid duplicate at same time? # For now, just add. Or remove existing note at this spot? # Simple overwrite logic: existing = [n for n in course.notes if abs(n.time - snapped_time) < 5] for n in existing: course.notes.remove(n) self.add_note(note_type, snapped_time, measure, beat) def add_note(self, note_type, time, measure, beat): course = self.get_current_course() if course: new_note = Note(note_type, time, measure, beat) course.notes.append(new_note) def remove_selected(self): course = self.get_current_course() if course and self.selected_notes: for n in self.selected_notes: if n in course.notes: course.notes.remove(n) self.selected_notes = []