| | import librosa |
| | import numpy as np |
| |
|
| | class ChordAnalyzer: |
| | def __init__(self): |
| | |
| | self.templates = self._generate_chord_templates() |
| |
|
| | def _generate_chord_templates(self): |
| | """ |
| | Membuat template chroma untuk berbagai jenis chord. |
| | 12 Nada: C, C#, D, D#, E, F, F#, G, G#, A, A#, B |
| | PRIORITIZED: Basic Major/Minor triads have higher matching priority |
| | """ |
| | templates = {} |
| | roots = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] |
| | |
| | |
| | |
| | |
| | qualities = { |
| | |
| | '': ([0, 4, 7], 1.3), |
| | 'm': ([0, 3, 7], 1.3), |
| | |
| | |
| | '5': ([0, 7], 1.1), |
| | 'sus4': ([0, 5, 7], 1.0), |
| | 'sus2': ([0, 2, 7], 1.0), |
| | |
| | |
| | 'maj7': ([0, 4, 7, 11], 0.95), |
| | 'm7': ([0, 3, 7, 10], 0.95), |
| | '7': ([0, 4, 7, 10], 0.95), |
| | |
| | |
| | 'dim': ([0, 3, 6], 0.9), |
| | 'aug': ([0, 4, 8], 0.9), |
| | '6': ([0, 4, 7, 9], 0.85), |
| | 'm6': ([0, 3, 7, 9], 0.85), |
| | 'add9': ([0, 4, 7, 2], 0.85), |
| | 'madd9': ([0, 3, 7, 2], 0.85), |
| | } |
| |
|
| | for i, root in enumerate(roots): |
| | for quality, (intervals, priority) in qualities.items(): |
| | |
| | vec = np.zeros(12) |
| | for j, interval in enumerate(intervals): |
| | idx = (i + interval) % 12 |
| | |
| | if j == 0: |
| | weight = 2.0 |
| | elif interval == 7: |
| | weight = 1.5 |
| | elif interval in [3, 4]: |
| | weight = 1.2 |
| | else: |
| | weight = 1.0 |
| | vec[idx] = weight |
| | |
| | |
| | vec *= priority |
| | |
| | chord_name = f"{root}{quality}" |
| | |
| | norm = np.linalg.norm(vec) |
| | if norm > 0: |
| | vec /= norm |
| | templates[chord_name] = vec |
| | |
| | return templates |
| |
|
| | def analyze(self, audio_path: str, sr=22050): |
| | """ |
| | Menganalisis file audio dan mengembalikan progresi chord dengan timestamp. |
| | """ |
| | print(f"Analyzing chords for: {audio_path}") |
| | try: |
| | y, sr = librosa.load(audio_path, sr=sr) |
| | |
| | |
| | y_harmonic, _ = librosa.effects.hpss(y) |
| | |
| | |
| | chroma = librosa.feature.chroma_cqt(y=y_harmonic, sr=sr, bins_per_octave=24) |
| | |
| | |
| | |
| | import scipy.ndimage |
| | chroma = scipy.ndimage.median_filter(chroma, size=(1, 21)) |
| | chroma = librosa.util.normalize(chroma) |
| | |
| | num_frames = chroma.shape[1] |
| | |
| | |
| | template_names = list(self.templates.keys()) |
| | template_matrix = np.array([self.templates[name] for name in template_names]) |
| | scores = np.dot(template_matrix, chroma) |
| | |
| | max_indices = np.argmax(scores, axis=0) |
| | max_scores = np.max(scores, axis=0) |
| | |
| | |
| | current_chord = None |
| | start_time = 0.0 |
| | |
| | THRESHOLD = 0.6 |
| | MIN_DURATION = 0.8 |
| | |
| | raw_segments = [] |
| | |
| | |
| | for i in range(num_frames): |
| | idx = max_indices[i] |
| | score = max_scores[i] |
| | timestamp = librosa.frames_to_time(i, sr=sr) |
| | chord_name = template_names[idx] if score > THRESHOLD else "N.C." |
| | |
| | if chord_name != current_chord: |
| | if current_chord is not None: |
| | raw_segments.append({ |
| | "chord": current_chord, |
| | "start": start_time, |
| | "end": timestamp, |
| | "duration": timestamp - start_time |
| | }) |
| | current_chord = chord_name |
| | start_time = timestamp |
| | |
| | |
| | if current_chord is not None: |
| | end_time = librosa.get_duration(y=y, sr=sr) |
| | raw_segments.append({ |
| | "chord": current_chord, |
| | "start": start_time, |
| | "end": end_time, |
| | "duration": end_time - start_time |
| | }) |
| |
|
| | |
| | final_results = [] |
| | if not raw_segments: return [] |
| |
|
| | |
| | for seg in raw_segments: |
| | if not final_results: |
| | final_results.append(seg) |
| | continue |
| |
|
| | prev = final_results[-1] |
| | |
| | |
| | |
| | if seg["chord"] == prev["chord"]: |
| | prev["end"] = seg["end"] |
| | prev["duration"] += seg["duration"] |
| | elif seg["duration"] < MIN_DURATION: |
| | |
| | prev["end"] = seg["end"] |
| | prev["duration"] += seg["duration"] |
| | else: |
| | final_results.append(seg) |
| | |
| | |
| | formatted_results = [] |
| | for r in final_results: |
| | formatted_results.append({ |
| | "chord": r["chord"], |
| | "start": round(r["start"], 2), |
| | "end": round(r["end"], 2) |
| | }) |
| |
|
| | return formatted_results |
| |
|
| | except Exception as e: |
| | print(f"Chord Analysis Error: {e}") |
| | return [] |
| |
|