gradio-pianoroll 0.0.1__tar.gz → 0.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. gradio_pianoroll-0.0.2/PKG-INFO +1015 -0
  2. gradio_pianoroll-0.0.2/README.md +990 -0
  3. gradio_pianoroll-0.0.2/TIMING_CONVERSIONS.md +327 -0
  4. gradio_pianoroll-0.0.2/backend/gradio_pianoroll/pianoroll.py +559 -0
  5. gradio_pianoroll-0.0.2/backend/gradio_pianoroll/pianoroll.pyi +729 -0
  6. {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/backend/gradio_pianoroll/templates/component/index.js +7593 -7077
  7. {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/backend/gradio_pianoroll/templates/component/style.css +1 -1
  8. gradio_pianoroll-0.0.2/demo/SYNTHESIZER_GUIDE.md +115 -0
  9. gradio_pianoroll-0.0.2/demo/app.py +673 -0
  10. gradio_pianoroll-0.0.2/demo/requirements.txt +3 -0
  11. gradio_pianoroll-0.0.2/demo/space.py +772 -0
  12. gradio_pianoroll-0.0.2/frontend/Index.svelte +201 -0
  13. {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/frontend/components/GridComponent.svelte +293 -218
  14. gradio_pianoroll-0.0.2/frontend/components/PianoRoll.svelte +818 -0
  15. {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/frontend/components/PlayheadComponent.svelte +3 -3
  16. {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/frontend/components/TimeLineComponent.svelte +18 -0
  17. {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/frontend/components/WaveformComponent.svelte +452 -304
  18. {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/frontend/utils/audioEngine.ts +139 -64
  19. gradio_pianoroll-0.0.2/frontend/utils/flicks.ts +194 -0
  20. {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/pyproject.toml +2 -2
  21. gradio_pianoroll-0.0.1/PKG-INFO +0 -337
  22. gradio_pianoroll-0.0.1/README.md +0 -312
  23. gradio_pianoroll-0.0.1/backend/gradio_pianoroll/pianoroll.py +0 -201
  24. gradio_pianoroll-0.0.1/backend/gradio_pianoroll/pianoroll.pyi +0 -292
  25. gradio_pianoroll-0.0.1/demo/app.py +0 -51
  26. gradio_pianoroll-0.0.1/demo/requirements.txt +0 -1
  27. gradio_pianoroll-0.0.1/demo/space.py +0 -150
  28. gradio_pianoroll-0.0.1/frontend/Index.svelte +0 -134
  29. gradio_pianoroll-0.0.1/frontend/components/PianoRoll.svelte +0 -374
  30. gradio_pianoroll-0.0.1/frontend/utils/flicks.ts +0 -76
  31. {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/.gitignore +0 -0
  32. {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/backend/gradio_pianoroll/__init__.py +0 -0
  33. {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/backend/gradio_pianoroll/templates/example/index.js +0 -0
  34. {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/backend/gradio_pianoroll/templates/example/style.css +0 -0
  35. {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/demo/__init__.py +0 -0
  36. {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/demo/css.css +0 -0
  37. {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/frontend/Example.svelte +0 -0
  38. {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/frontend/components/DebugComponent.svelte +0 -0
  39. {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/frontend/components/KeyboardComponent.svelte +0 -0
  40. {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/frontend/components/Toolbar.svelte +0 -0
  41. {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/frontend/gradio.config.js +0 -0
  42. {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/frontend/package-lock.json +0 -0
  43. {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/frontend/package.json +0 -0
  44. {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/frontend/tsconfig.json +0 -0
@@ -0,0 +1,1015 @@
1
+ Metadata-Version: 2.4
2
+ Name: gradio_pianoroll
3
+ Version: 0.0.2
4
+ Summary: A PianoRoll Component for Gradio.
5
+ Author-email: crlotwhite <crlotwhite@gmail.com>
6
+ License-Expression: Apache-2.0
7
+ Keywords: gradio-custom-component,gradio-template-Fallback
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Topic :: Scientific/Engineering
17
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
+ Classifier: Topic :: Scientific/Engineering :: Visualization
19
+ Requires-Python: >=3.10
20
+ Requires-Dist: gradio<6.0,>=4.0
21
+ Provides-Extra: dev
22
+ Requires-Dist: build; extra == 'dev'
23
+ Requires-Dist: twine; extra == 'dev'
24
+ Description-Content-Type: text/markdown
25
+
26
+ ---
27
+ tags: [gradio-custom-component, ]
28
+ title: gradio_pianoroll
29
+ short_description: A PianoRoll Component for
30
+ colorFrom: blue
31
+ colorTo: yellow
32
+ sdk: gradio
33
+ pinned: false
34
+ app_file: space.py
35
+ ---
36
+
37
+ # `gradio_pianoroll`
38
+ <a href="https://pypi.org/project/gradio_pianoroll/" target="_blank"><img alt="PyPI - Version" src="https://img.shields.io/pypi/v/gradio_pianoroll"></a>
39
+
40
+ A PianoRoll Component for Gradio.
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ pip install gradio_pianoroll
46
+ ```
47
+
48
+ ## Usage
49
+
50
+ ```python
51
+ import gradio as gr
52
+ import numpy as np
53
+ import io
54
+ import base64
55
+ import wave
56
+ import tempfile
57
+ import os
58
+ from gradio_pianoroll import PianoRoll
59
+
60
+ # 신디사이저 설정
61
+ SAMPLE_RATE = 44100
62
+ MAX_DURATION = 10.0 # 최대 10초
63
+
64
+ def midi_to_frequency(midi_note):
65
+ """MIDI 노트 번호를 주파수로 변환 (A4 = 440Hz)"""
66
+ return 440.0 * (2.0 ** ((midi_note - 69) / 12.0))
67
+
68
+ def create_adsr_envelope(attack, decay, sustain, release, duration, sample_rate):
69
+ """ADSR 엔벨로프를 생성"""
70
+ total_samples = int(duration * sample_rate)
71
+ attack_samples = int(attack * sample_rate)
72
+ decay_samples = int(decay * sample_rate)
73
+ release_samples = int(release * sample_rate)
74
+ sustain_samples = total_samples - attack_samples - decay_samples - release_samples
75
+
76
+ # 지속 구간이 음수가 되지 않도록 조정
77
+ if sustain_samples < 0:
78
+ sustain_samples = 0
79
+ total_samples = attack_samples + decay_samples + release_samples
80
+
81
+ envelope = np.zeros(total_samples)
82
+
83
+ # Attack phase
84
+ if attack_samples > 0:
85
+ envelope[:attack_samples] = np.linspace(0, 1, attack_samples)
86
+
87
+ # Decay phase
88
+ if decay_samples > 0:
89
+ start_idx = attack_samples
90
+ end_idx = attack_samples + decay_samples
91
+ envelope[start_idx:end_idx] = np.linspace(1, sustain, decay_samples)
92
+
93
+ # Sustain phase
94
+ if sustain_samples > 0:
95
+ start_idx = attack_samples + decay_samples
96
+ end_idx = start_idx + sustain_samples
97
+ envelope[start_idx:end_idx] = sustain
98
+
99
+ # Release phase
100
+ if release_samples > 0:
101
+ start_idx = attack_samples + decay_samples + sustain_samples
102
+ envelope[start_idx:] = np.linspace(sustain, 0, release_samples)
103
+
104
+ return envelope
105
+
106
+ def generate_sine_wave(frequency, duration, sample_rate):
107
+ """사인파 생성"""
108
+ t = np.linspace(0, duration, int(duration * sample_rate), False)
109
+ return np.sin(2 * np.pi * frequency * t)
110
+
111
+ def generate_sawtooth_wave(frequency, duration, sample_rate):
112
+ """톱니파 생성"""
113
+ t = np.linspace(0, duration, int(duration * sample_rate), False)
114
+ # 2 * (t * frequency - np.floor(0.5 + t * frequency))
115
+ return 2 * (t * frequency % 1) - 1
116
+
117
+ def generate_square_wave(frequency, duration, sample_rate):
118
+ """사각파 생성"""
119
+ t = np.linspace(0, duration, int(duration * sample_rate), False)
120
+ return np.sign(np.sin(2 * np.pi * frequency * t))
121
+
122
+ def generate_triangle_wave(frequency, duration, sample_rate):
123
+ """삼각파 생성"""
124
+ t = np.linspace(0, duration, int(duration * sample_rate), False)
125
+ return 2 * np.abs(2 * (t * frequency % 1) - 1) - 1
126
+
127
+ def generate_harmonic_wave(frequency, duration, sample_rate, harmonics=5):
128
+ """하모닉을 포함한 복합 파형 생성"""
129
+ t = np.linspace(0, duration, int(duration * sample_rate), False)
130
+ wave = np.zeros_like(t)
131
+
132
+ # 기본 주파수
133
+ wave += np.sin(2 * np.pi * frequency * t)
134
+
135
+ # 하모닉 추가 (각 하모닉의 진폭은 1/n로 감소)
136
+ for n in range(2, harmonics + 1):
137
+ amplitude = 1.0 / n
138
+ wave += amplitude * np.sin(2 * np.pi * frequency * n * t)
139
+
140
+ # 정규화
141
+ wave = wave / np.max(np.abs(wave))
142
+ return wave
143
+
144
+ def generate_fm_wave(frequency, duration, sample_rate, mod_freq=5.0, mod_depth=2.0):
145
+ """FM 합성 파형 생성"""
146
+ t = np.linspace(0, duration, int(duration * sample_rate), False)
147
+
148
+ # Modulator
149
+ modulator = mod_depth * np.sin(2 * np.pi * mod_freq * t)
150
+
151
+ # Carrier with frequency modulation
152
+ carrier = np.sin(2 * np.pi * frequency * t + modulator)
153
+
154
+ return carrier
155
+
156
+ def generate_complex_wave(frequency, duration, sample_rate, wave_type='complex'):
157
+ """복합적인 파형 생성 (여러 기법 조합)"""
158
+ if wave_type == 'sine':
159
+ return generate_sine_wave(frequency, duration, sample_rate)
160
+ elif wave_type == 'sawtooth':
161
+ return generate_sawtooth_wave(frequency, duration, sample_rate)
162
+ elif wave_type == 'square':
163
+ return generate_square_wave(frequency, duration, sample_rate)
164
+ elif wave_type == 'triangle':
165
+ return generate_triangle_wave(frequency, duration, sample_rate)
166
+ elif wave_type == 'harmonic':
167
+ return generate_harmonic_wave(frequency, duration, sample_rate, harmonics=7)
168
+ elif wave_type == 'fm':
169
+ return generate_fm_wave(frequency, duration, sample_rate, mod_freq=frequency * 0.1, mod_depth=3.0)
170
+ else: # 'complex' - 여러 파형 조합
171
+ # 기본 sawtooth + 하모닉 + 약간의 FM
172
+ base = generate_sawtooth_wave(frequency, duration, sample_rate) * 0.6
173
+ harmonic = generate_harmonic_wave(frequency, duration, sample_rate, harmonics=4) * 0.3
174
+ fm = generate_fm_wave(frequency, duration, sample_rate, mod_freq=frequency * 0.05, mod_depth=1.0) * 0.1
175
+
176
+ return base + harmonic + fm
177
+
178
+ def synthesize_audio(piano_roll_data, attack=0.01, decay=0.1, sustain=0.7, release=0.3, wave_type='complex'):
179
+ """피아노롤 데이터로부터 오디오를 합성"""
180
+ if not piano_roll_data or 'notes' not in piano_roll_data or not piano_roll_data['notes']:
181
+ return None
182
+
183
+ notes = piano_roll_data['notes']
184
+ tempo = piano_roll_data.get('tempo', 120)
185
+ pixels_per_beat = piano_roll_data.get('pixelsPerBeat', 80)
186
+
187
+ # 전체 길이 계산 (마지막 노트의 끝까지)
188
+ max_end_time = 0
189
+ for note in notes:
190
+ # 픽셀을 초로 변환 (템포와 픽셀당 비트 수 고려)
191
+ start_seconds = (note['start'] / pixels_per_beat) * (60.0 / tempo)
192
+ duration_seconds = (note['duration'] / pixels_per_beat) * (60.0 / tempo)
193
+ end_time = start_seconds + duration_seconds
194
+ max_end_time = max(max_end_time, end_time)
195
+
196
+ # 최대 길이 제한
197
+ total_duration = min(max_end_time + 1.0, MAX_DURATION) # 1초 여유 추가
198
+ total_samples = int(total_duration * SAMPLE_RATE)
199
+
200
+ # 최종 오디오 버퍼
201
+ audio_buffer = np.zeros(total_samples)
202
+
203
+ # 각 노트 처리
204
+ for i, note in enumerate(notes):
205
+ try:
206
+ # 노트 속성
207
+ pitch = note['pitch']
208
+ velocity = note.get('velocity', 100)
209
+
210
+ # 시간 계산
211
+ start_seconds = (note['start'] / pixels_per_beat) * (60.0 / tempo)
212
+ duration_seconds = (note['duration'] / pixels_per_beat) * (60.0 / tempo)
213
+
214
+ # 범위 체크
215
+ if start_seconds >= total_duration:
216
+ continue
217
+
218
+ # 지속 시간이 전체 길이를 초과하지 않도록 조정
219
+ if start_seconds + duration_seconds > total_duration:
220
+ duration_seconds = total_duration - start_seconds
221
+
222
+ if duration_seconds <= 0:
223
+ continue
224
+
225
+ # 주파수 계산
226
+ frequency = midi_to_frequency(pitch)
227
+
228
+ # 볼륨 계산 (velocity를 0-1로 정규화)
229
+ volume = velocity / 127.0
230
+
231
+ # 모든 노트에 동일한 파형 타입 사용 (일관성 유지)
232
+ # 복합 파형 생성
233
+ base_wave = generate_complex_wave(frequency, duration_seconds, SAMPLE_RATE, wave_type)
234
+
235
+ # 추가 효과: 비브라토 (주파수 변조)
236
+ t = np.linspace(0, duration_seconds, len(base_wave), False)
237
+ vibrato_freq = 4.5 # 4.5Hz 비브라토
238
+ vibrato_depth = 0.02 # 2% 주파수 변조
239
+ vibrato = 1 + vibrato_depth * np.sin(2 * np.pi * vibrato_freq * t)
240
+
241
+ # 비브라토를 파형에 적용 (간단한 근사)
242
+ vibrato_wave = base_wave * vibrato
243
+
244
+ # 추가 효과: 트레몰로 (진폭 변조)
245
+ tremolo_freq = 3.0 # 3Hz 트레몰로
246
+ tremolo_depth = 0.1 # 10% 진폭 변조
247
+ tremolo = 1 + tremolo_depth * np.sin(2 * np.pi * tremolo_freq * t)
248
+
249
+ # 트레몰로 적용
250
+ final_wave = vibrato_wave * tremolo
251
+
252
+ # ADSR 엔벨로프 적용
253
+ envelope = create_adsr_envelope(attack, decay, sustain, release, duration_seconds, SAMPLE_RATE)
254
+
255
+ # 엔벨로프와 파형 길이 맞춤
256
+ min_length = min(len(final_wave), len(envelope))
257
+ note_audio = final_wave[:min_length] * envelope[:min_length] * volume * 0.25 # 볼륨 조절
258
+
259
+ # 오디오 버퍼에 추가
260
+ start_sample = int(start_seconds * SAMPLE_RATE)
261
+ end_sample = start_sample + len(note_audio)
262
+
263
+ # 버퍼 범위 내에서만 추가
264
+ if start_sample < total_samples:
265
+ end_sample = min(end_sample, total_samples)
266
+ audio_length = end_sample - start_sample
267
+ if audio_length > 0:
268
+ audio_buffer[start_sample:end_sample] += note_audio[:audio_length]
269
+
270
+ except Exception as e:
271
+ print(f"노트 처리 중 오류: {e}")
272
+ continue
273
+
274
+ # 클리핑 방지 (normalize)
275
+ max_amplitude = np.max(np.abs(audio_buffer))
276
+ if max_amplitude > 0:
277
+ audio_buffer = audio_buffer / max_amplitude * 0.9 # 90%로 제한
278
+
279
+ return audio_buffer
280
+
281
+ def audio_to_base64_wav(audio_data, sample_rate):
282
+ """오디오 데이터를 base64 인코딩된 WAV로 변환"""
283
+ if audio_data is None or len(audio_data) == 0:
284
+ return None
285
+
286
+ # 16비트 PCM으로 변환
287
+ audio_16bit = (audio_data * 32767).astype(np.int16)
288
+
289
+ # WAV 파일을 메모리에 생성
290
+ buffer = io.BytesIO()
291
+ with wave.open(buffer, 'wb') as wav_file:
292
+ wav_file.setnchannels(1) # 모노
293
+ wav_file.setsampwidth(2) # 16비트
294
+ wav_file.setframerate(sample_rate)
295
+ wav_file.writeframes(audio_16bit.tobytes())
296
+
297
+ # base64 인코딩
298
+ buffer.seek(0)
299
+ wav_data = buffer.read()
300
+ base64_data = base64.b64encode(wav_data).decode('utf-8')
301
+
302
+ return f"data:audio/wav;base64,{base64_data}"
303
+
304
+ def calculate_waveform_data(audio_data, pixels_per_beat, tempo, target_width=1000):
305
+ """오디오 데이터로부터 웨이브폼 시각화 데이터를 계산"""
306
+ if audio_data is None or len(audio_data) == 0:
307
+ return None
308
+
309
+ # 오디오 총 길이 (초)
310
+ audio_duration = len(audio_data) / SAMPLE_RATE
311
+
312
+ # 총 픽셀 길이 계산 (템포와 픽셀당 비트 기반)
313
+ total_pixels = (tempo / 60) * pixels_per_beat * audio_duration
314
+
315
+ # 각 픽셀당 샘플 수 계산
316
+ samples_per_pixel = len(audio_data) / total_pixels
317
+
318
+ waveform_points = []
319
+
320
+ # 각 픽셀에 대해 min/max 값 계산
321
+ for pixel in range(int(total_pixels)):
322
+ start_sample = int(pixel * samples_per_pixel)
323
+ end_sample = int((pixel + 1) * samples_per_pixel)
324
+ end_sample = min(end_sample, len(audio_data))
325
+
326
+ if start_sample >= len(audio_data):
327
+ break
328
+
329
+ if start_sample < end_sample:
330
+ # 해당 픽셀 범위의 오디오 데이터
331
+ pixel_data = audio_data[start_sample:end_sample]
332
+
333
+ # min, max 값 계산
334
+ min_val = float(np.min(pixel_data))
335
+ max_val = float(np.max(pixel_data))
336
+
337
+ # 시간 정보 (픽셀 위치)
338
+ time_position = pixel
339
+
340
+ waveform_points.append({
341
+ 'x': time_position,
342
+ 'min': min_val,
343
+ 'max': max_val
344
+ })
345
+
346
+ return waveform_points
347
+
348
+ def convert_basic(piano_roll):
349
+ """기본 변환 함수 (첫 번째 탭용)"""
350
+ print("=== Basic Convert function called ===")
351
+ print("Received piano_roll:")
352
+ print(piano_roll)
353
+ print("Type:", type(piano_roll))
354
+ return piano_roll
355
+
356
+ def synthesize_and_play(piano_roll, attack, decay, sustain, release, wave_type='complex'):
357
+ """신디사이저로 오디오를 생성하고 피아노롤에 전달"""
358
+ print("=== Synthesize function called ===")
359
+ print("Piano roll data:", piano_roll)
360
+ print(f"ADSR: A={attack}, D={decay}, S={sustain}, R={release}")
361
+ print(f"Wave Type: {wave_type}")
362
+
363
+ # 오디오 합성
364
+ audio_data = synthesize_audio(piano_roll, attack, decay, sustain, release, wave_type)
365
+
366
+ if audio_data is None:
367
+ print("오디오 생성 실패")
368
+ return piano_roll, "오디오 생성 실패", None
369
+
370
+ # base64로 변환 (피아노롤용)
371
+ audio_base64 = audio_to_base64_wav(audio_data, SAMPLE_RATE)
372
+
373
+ # gradio Audio 컴포넌트용 WAV 파일 생성
374
+ gradio_audio_path = create_temp_wav_file(audio_data, SAMPLE_RATE)
375
+
376
+ # 피아노롤 데이터에 오디오 추가
377
+ updated_piano_roll = piano_roll.copy() if piano_roll else {}
378
+ updated_piano_roll['audio_data'] = audio_base64
379
+ updated_piano_roll['use_backend_audio'] = True
380
+
381
+ print(f"🔊 [synthesize_and_play] Setting backend audio data:")
382
+ print(f" - audio_data length: {len(audio_base64) if audio_base64 else 0}")
383
+ print(f" - use_backend_audio: {updated_piano_roll['use_backend_audio']}")
384
+ print(f" - audio_base64 preview: {audio_base64[:50] + '...' if audio_base64 else 'None'}")
385
+
386
+ # 웨이브폼 데이터 계산
387
+ pixels_per_beat = updated_piano_roll.get('pixelsPerBeat', 80)
388
+ tempo = updated_piano_roll.get('tempo', 120)
389
+ waveform_data = calculate_waveform_data(audio_data, pixels_per_beat, tempo)
390
+
391
+ # 곡선 데이터 예시 (피치 곡선 + 웨이브폼 데이터)
392
+ curve_data = {}
393
+
394
+ # 웨이브폼 데이터 추가
395
+ if waveform_data:
396
+ curve_data['waveform_data'] = waveform_data
397
+ print(f"웨이브폼 데이터 생성: {len(waveform_data)} 포인트")
398
+
399
+ # 피치 곡선 데이터 (기존)
400
+ if 'notes' in updated_piano_roll and updated_piano_roll['notes']:
401
+ pitch_curve = []
402
+ for note in updated_piano_roll['notes']:
403
+ # 간단한 예시: 노트의 피치를 기반으로 곡선 생성
404
+ base_pitch = note['pitch']
405
+ # 약간의 비브라토 효과
406
+ curve_points = [base_pitch + 0.5 * np.sin(i * 0.5) for i in range(10)]
407
+ pitch_curve.extend(curve_points)
408
+
409
+ curve_data['pitch_curve'] = pitch_curve[:100] # 최대 100개 포인트로 제한
410
+
411
+ updated_piano_roll['curve_data'] = curve_data
412
+
413
+ # 세그먼트 데이터 예시 (발음 타이밍)
414
+ if 'notes' in updated_piano_roll and updated_piano_roll['notes']:
415
+ segment_data = []
416
+
417
+ for i, note in enumerate(updated_piano_roll['notes']):
418
+ start_seconds = (note['start'] / pixels_per_beat) * (60.0 / tempo)
419
+ duration_seconds = (note['duration'] / pixels_per_beat) * (60.0 / tempo)
420
+
421
+ segment_data.append({
422
+ 'start': start_seconds,
423
+ 'end': start_seconds + duration_seconds,
424
+ 'type': 'note',
425
+ 'value': note.get('lyric', f"Note_{i+1}"),
426
+ 'confidence': 0.95
427
+ })
428
+
429
+ updated_piano_roll['segment_data'] = segment_data
430
+
431
+ print(f"오디오 생성 완료: {len(audio_data)} 샘플")
432
+ if waveform_data:
433
+ print(f"웨이브폼 포인트: {len(waveform_data)}개")
434
+
435
+ status_message = f"오디오 생성 완료 ({wave_type} 파형): {len(audio_data)} 샘플, 길이: {len(audio_data)/SAMPLE_RATE:.2f}초"
436
+
437
+ return updated_piano_roll, status_message, gradio_audio_path
438
+
439
+ def create_temp_wav_file(audio_data, sample_rate):
440
+ """gradio Audio 컴포넌트용 임시 WAV 파일 생성"""
441
+ if audio_data is None or len(audio_data) == 0:
442
+ return None
443
+
444
+ try:
445
+ # 16비트 PCM으로 변환
446
+ audio_16bit = (audio_data * 32767).astype(np.int16)
447
+
448
+ # 임시 파일 생성
449
+ temp_fd, temp_path = tempfile.mkstemp(suffix='.wav')
450
+
451
+ with wave.open(temp_path, 'wb') as wav_file:
452
+ wav_file.setnchannels(1) # 모노
453
+ wav_file.setsampwidth(2) # 16비트
454
+ wav_file.setframerate(sample_rate)
455
+ wav_file.writeframes(audio_16bit.tobytes())
456
+
457
+ # 파일 디스크립터 닫기
458
+ os.close(temp_fd)
459
+
460
+ return temp_path
461
+ except Exception as e:
462
+ print(f"임시 WAV 파일 생성 오류: {e}")
463
+ return None
464
+
465
+ def clear_and_regenerate_waveform(piano_roll, attack, decay, sustain, release, wave_type='complex'):
466
+ """웨이브폼을 지우고 다시 생성"""
467
+ print("=== Clear and Regenerate Waveform ===")
468
+
469
+ # 먼저 웨이브폼 데이터를 지움
470
+ cleared_piano_roll = piano_roll.copy() if piano_roll else {}
471
+ cleared_piano_roll['curve_data'] = {} # 곡선 데이터 초기화
472
+ cleared_piano_roll['audio_data'] = None # 오디오 데이터 초기화
473
+ cleared_piano_roll['use_backend_audio'] = False # 백엔드 오디오 비활성화
474
+
475
+ # 잠시 대기를 위한 메시지
476
+ yield cleared_piano_roll, "웨이브폼을 지우는 중...", None
477
+
478
+ # 그 다음 새로운 웨이브폼 생성
479
+ result_piano_roll, status_message, gradio_audio_path = synthesize_and_play(piano_roll, attack, decay, sustain, release, wave_type)
480
+
481
+ yield result_piano_roll, f"재생성 완료! {status_message}", gradio_audio_path
482
+
483
+ # Gradio 인터페이스
484
+ with gr.Blocks(title="PianoRoll with Synthesizer Demo") as demo:
485
+ gr.Markdown("# 🎹 Gradio PianoRoll with Synthesizer")
486
+ gr.Markdown("피아노롤 컴포넌트와 신디사이저 기능을 테스트해보세요!")
487
+
488
+ with gr.Tabs():
489
+ # 첫 번째 탭: 기본 데모
490
+ with gr.TabItem("🎼 Basic Demo"):
491
+ gr.Markdown("## 기본 피아노롤 데모")
492
+
493
+ with gr.Row():
494
+ with gr.Column():
495
+ # 초기값 설정
496
+ initial_value_basic = {
497
+ "notes": [
498
+ {
499
+ "start": 80,
500
+ "duration": 80,
501
+ "pitch": 60,
502
+ "velocity": 100,
503
+ "lyric": "안녕"
504
+ },
505
+ {
506
+ "start": 160,
507
+ "duration": 160,
508
+ "pitch": 64,
509
+ "velocity": 90,
510
+ "lyric": "하세요"
511
+ }
512
+ ],
513
+ "tempo": 120,
514
+ "timeSignature": {"numerator": 4, "denominator": 4},
515
+ "editMode": "select",
516
+ "snapSetting": "1/4"
517
+ }
518
+ piano_roll_basic = PianoRoll(
519
+ height=600,
520
+ width=1000,
521
+ value=initial_value_basic,
522
+ elem_id="piano_roll_basic", # 고유 ID 부여
523
+ use_backend_audio=False # 프론트엔드 오디오 엔진 사용
524
+ )
525
+
526
+ with gr.Row():
527
+ with gr.Column():
528
+ output_json_basic = gr.JSON()
529
+
530
+ with gr.Row():
531
+ with gr.Column():
532
+ btn_basic = gr.Button("🔄 Convert & Debug", variant="primary")
533
+
534
+ # 기본 탭 이벤트
535
+ btn_basic.click(
536
+ fn=convert_basic,
537
+ inputs=piano_roll_basic,
538
+ outputs=output_json_basic,
539
+ show_progress=True
540
+ )
541
+
542
+ # 두 번째 탭: 신디사이저 데모
543
+ with gr.TabItem("🎵 Synthesizer Demo"):
544
+ gr.Markdown("## 신디사이저가 포함된 피아노롤 데모")
545
+ gr.Markdown("노트를 편집한 후 '🎶 Synthesize Audio' 버튼을 클릭하면 오디오가 생성되어 재생됩니다!")
546
+
547
+ with gr.Row():
548
+ with gr.Column(scale=3):
549
+ # 신디사이저용 초기값
550
+ initial_value_synth = {
551
+ "notes": [
552
+ {
553
+ "start": 0,
554
+ "duration": 160,
555
+ "pitch": 60, # C4
556
+ "velocity": 100,
557
+ "lyric": "도"
558
+ },
559
+ {
560
+ "start": 160,
561
+ "duration": 160,
562
+ "pitch": 62, # D4
563
+ "velocity": 100,
564
+ "lyric": "레"
565
+ },
566
+ {
567
+ "start": 320,
568
+ "duration": 160,
569
+ "pitch": 64, # E4
570
+ "velocity": 100,
571
+ "lyric": "미"
572
+ },
573
+ {
574
+ "start": 480,
575
+ "duration": 160,
576
+ "pitch": 65, # F4
577
+ "velocity": 100,
578
+ "lyric": "파"
579
+ }
580
+ ],
581
+ "tempo": 120,
582
+ "timeSignature": {"numerator": 4, "denominator": 4},
583
+ "editMode": "select",
584
+ "snapSetting": "1/4",
585
+ "curve_data": {}, # 초기에는 빈 곡선 데이터
586
+ "use_backend_audio": False # 초기에는 백엔드 오디오 비활성화
587
+ }
588
+ piano_roll_synth = PianoRoll(
589
+ height=600,
590
+ width=1000,
591
+ value=initial_value_synth,
592
+ elem_id="piano_roll_synth", # 고유 ID 부여
593
+ use_backend_audio=False # 초기에는 프론트엔드 엔진 사용, synthesize 시 백엔드로 전환
594
+ )
595
+
596
+ with gr.Column(scale=1):
597
+ gr.Markdown("### 🎛️ ADSR 설정")
598
+ attack_slider = gr.Slider(
599
+ minimum=0.001,
600
+ maximum=1.0,
601
+ value=0.01,
602
+ step=0.001,
603
+ label="Attack (초)"
604
+ )
605
+ decay_slider = gr.Slider(
606
+ minimum=0.001,
607
+ maximum=1.0,
608
+ value=0.1,
609
+ step=0.001,
610
+ label="Decay (초)"
611
+ )
612
+ sustain_slider = gr.Slider(
613
+ minimum=0.0,
614
+ maximum=1.0,
615
+ value=0.7,
616
+ step=0.01,
617
+ label="Sustain (레벨)"
618
+ )
619
+ release_slider = gr.Slider(
620
+ minimum=0.001,
621
+ maximum=2.0,
622
+ value=0.3,
623
+ step=0.001,
624
+ label="Release (초)"
625
+ )
626
+
627
+ gr.Markdown("### 🎵 파형 설정")
628
+ wave_type_dropdown = gr.Dropdown(
629
+ choices=[
630
+ ("복합 파형 (Complex)", "complex"),
631
+ ("하모닉 합성 (Harmonic)", "harmonic"),
632
+ ("FM 합성 (FM)", "fm"),
633
+ ("톱니파 (Sawtooth)", "sawtooth"),
634
+ ("사각파 (Square)", "square"),
635
+ ("삼각파 (Triangle)", "triangle"),
636
+ ("사인파 (Sine)", "sine")
637
+ ],
638
+ value="complex",
639
+ label="파형 타입",
640
+ info="각 노트는 순환적으로 다른 파형을 사용합니다"
641
+ )
642
+
643
+ with gr.Row():
644
+ with gr.Column():
645
+ btn_synthesize = gr.Button("🎶 Synthesize Audio", variant="primary", size="lg")
646
+ status_text = gr.Textbox(label="상태", interactive=False)
647
+
648
+ with gr.Row():
649
+ with gr.Column():
650
+ btn_regenerate = gr.Button("🔄 웨이브폼 재생성", variant="secondary", size="lg")
651
+
652
+ # 비교용 gradio Audio 컴포넌트 추가
653
+ with gr.Row():
654
+ with gr.Column():
655
+ gr.Markdown("### 🔊 비교용 Gradio Audio 재생")
656
+ gradio_audio_output = gr.Audio(
657
+ label="백엔드에서 생성된 오디오 (비교용)",
658
+ type="filepath",
659
+ interactive=False
660
+ )
661
+
662
+ with gr.Row():
663
+ with gr.Column():
664
+ output_json_synth = gr.JSON(label="결과 데이터")
665
+
666
+ # 신디사이저 탭 이벤트
667
+ btn_synthesize.click(
668
+ fn=synthesize_and_play,
669
+ inputs=[
670
+ piano_roll_synth,
671
+ attack_slider,
672
+ decay_slider,
673
+ sustain_slider,
674
+ release_slider,
675
+ wave_type_dropdown
676
+ ],
677
+ outputs=[piano_roll_synth, status_text, gradio_audio_output],
678
+ show_progress=True
679
+ )
680
+
681
+ # 웨이브폼 재생성 버튼 이벤트
682
+ btn_regenerate.click(
683
+ fn=clear_and_regenerate_waveform,
684
+ inputs=[
685
+ piano_roll_synth,
686
+ attack_slider,
687
+ decay_slider,
688
+ sustain_slider,
689
+ release_slider,
690
+ wave_type_dropdown
691
+ ],
692
+ outputs=[piano_roll_synth, status_text, gradio_audio_output],
693
+ show_progress=True
694
+ )
695
+
696
+ # 이벤트 로깅을 위한 함수들
697
+ def log_play_event(event_data):
698
+ print("🎵 Play event triggered:", event_data)
699
+ return f"재생 시작됨: {event_data}"
700
+
701
+ def log_pause_event(event_data):
702
+ print("⏸️ Pause event triggered:", event_data)
703
+ return f"일시정지됨: {event_data}"
704
+
705
+ def log_stop_event(event_data):
706
+ print("⏹️ Stop event triggered:", event_data)
707
+ return f"정지됨: {event_data}"
708
+
709
+ def log_input_event(lyric_data):
710
+ print("✏️ Lyric input event triggered:", lyric_data)
711
+ return f"가사 입력: {lyric_data}"
712
+
713
+ # 이벤트 리스너 설정
714
+ piano_roll_synth.play(log_play_event, outputs=status_text)
715
+ piano_roll_synth.pause(log_pause_event, outputs=status_text)
716
+ piano_roll_synth.stop(log_stop_event, outputs=status_text)
717
+ piano_roll_synth.input(log_input_event, outputs=status_text)
718
+
719
+ # 노트 변경 시 JSON 출력 업데이트
720
+ piano_roll_synth.change(lambda x: x, inputs=piano_roll_synth, outputs=output_json_synth)
721
+
722
+ if __name__ == "__main__":
723
+ demo.launch()
724
+
725
+ ```
726
+
727
+ ## `PianoRoll`
728
+
729
+ ### Initialization
730
+
731
+ <table>
732
+ <thead>
733
+ <tr>
734
+ <th align="left">name</th>
735
+ <th align="left" style="width: 25%;">type</th>
736
+ <th align="left">default</th>
737
+ <th align="left">description</th>
738
+ </tr>
739
+ </thead>
740
+ <tbody>
741
+ <tr>
742
+ <td align="left"><code>value</code></td>
743
+ <td align="left" style="width: 25%;">
744
+
745
+ ```python
746
+ dict | None
747
+ ```
748
+
749
+ </td>
750
+ <td align="left"><code>None</code></td>
751
+ <td align="left">default MIDI notes data to provide in piano roll. If a function is provided, the function will be called each time the app loads to set the initial value of this component.</td>
752
+ </tr>
753
+
754
+ <tr>
755
+ <td align="left"><code>audio_data</code></td>
756
+ <td align="left" style="width: 25%;">
757
+
758
+ ```python
759
+ str | None
760
+ ```
761
+
762
+ </td>
763
+ <td align="left"><code>None</code></td>
764
+ <td align="left">백엔드에서 전달받은 오디오 데이터 (base64 인코딩된 오디오 또는 URL)</td>
765
+ </tr>
766
+
767
+ <tr>
768
+ <td align="left"><code>curve_data</code></td>
769
+ <td align="left" style="width: 25%;">
770
+
771
+ ```python
772
+ dict | None
773
+ ```
774
+
775
+ </td>
776
+ <td align="left"><code>None</code></td>
777
+ <td align="left">백엔드에서 전달받은 선형 데이터 (피치 곡선, loudness 곡선 등)</td>
778
+ </tr>
779
+
780
+ <tr>
781
+ <td align="left"><code>segment_data</code></td>
782
+ <td align="left" style="width: 25%;">
783
+
784
+ ```python
785
+ list | None
786
+ ```
787
+
788
+ </td>
789
+ <td align="left"><code>None</code></td>
790
+ <td align="left">백엔드에서 전달받은 구간 데이터 (발음 타이밍 등)</td>
791
+ </tr>
792
+
793
+ <tr>
794
+ <td align="left"><code>use_backend_audio</code></td>
795
+ <td align="left" style="width: 25%;">
796
+
797
+ ```python
798
+ bool
799
+ ```
800
+
801
+ </td>
802
+ <td align="left"><code>False</code></td>
803
+ <td align="left">백엔드 오디오를 사용할지 여부 (True시 프론트엔드 오디오 엔진 비활성화)</td>
804
+ </tr>
805
+
806
+ <tr>
807
+ <td align="left"><code>label</code></td>
808
+ <td align="left" style="width: 25%;">
809
+
810
+ ```python
811
+ str | I18nData | None
812
+ ```
813
+
814
+ </td>
815
+ <td align="left"><code>None</code></td>
816
+ <td align="left">the label for this component, displayed above the component if `show_label` is `True` and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component corresponds to.</td>
817
+ </tr>
818
+
819
+ <tr>
820
+ <td align="left"><code>every</code></td>
821
+ <td align="left" style="width: 25%;">
822
+
823
+ ```python
824
+ "Timer | float | None"
825
+ ```
826
+
827
+ </td>
828
+ <td align="left"><code>None</code></td>
829
+ <td align="left">Continously calls `value` to recalculate it if `value` is a function (has no effect otherwise). Can provide a Timer whose tick resets `value`, or a float that provides the regular interval for the reset Timer.</td>
830
+ </tr>
831
+
832
+ <tr>
833
+ <td align="left"><code>inputs</code></td>
834
+ <td align="left" style="width: 25%;">
835
+
836
+ ```python
837
+ Component | Sequence[Component] | set[Component] | None
838
+ ```
839
+
840
+ </td>
841
+ <td align="left"><code>None</code></td>
842
+ <td align="left">Components that are used as inputs to calculate `value` if `value` is a function (has no effect otherwise). `value` is recalculated any time the inputs change.</td>
843
+ </tr>
844
+
845
+ <tr>
846
+ <td align="left"><code>show_label</code></td>
847
+ <td align="left" style="width: 25%;">
848
+
849
+ ```python
850
+ bool | None
851
+ ```
852
+
853
+ </td>
854
+ <td align="left"><code>None</code></td>
855
+ <td align="left">if True, will display label.</td>
856
+ </tr>
857
+
858
+ <tr>
859
+ <td align="left"><code>scale</code></td>
860
+ <td align="left" style="width: 25%;">
861
+
862
+ ```python
863
+ int | None
864
+ ```
865
+
866
+ </td>
867
+ <td align="left"><code>None</code></td>
868
+ <td align="left">relative size compared to adjacent Components. For example if Components A and B are in a Row, and A has scale=2, and B has scale=1, A will be twice as wide as B. Should be an integer. scale applies in Rows, and to top-level Components in Blocks where fill_height=True.</td>
869
+ </tr>
870
+
871
+ <tr>
872
+ <td align="left"><code>min_width</code></td>
873
+ <td align="left" style="width: 25%;">
874
+
875
+ ```python
876
+ int
877
+ ```
878
+
879
+ </td>
880
+ <td align="left"><code>160</code></td>
881
+ <td align="left">minimum pixel width, will wrap if not sufficient screen space to satisfy this value. If a certain scale value results in this Component being narrower than min_width, the min_width parameter will be respected first.</td>
882
+ </tr>
883
+
884
+ <tr>
885
+ <td align="left"><code>interactive</code></td>
886
+ <td align="left" style="width: 25%;">
887
+
888
+ ```python
889
+ bool | None
890
+ ```
891
+
892
+ </td>
893
+ <td align="left"><code>None</code></td>
894
+ <td align="left">if True, will be rendered as an editable piano roll; if False, editing will be disabled. If not provided, this is inferred based on whether the component is used as an input or output.</td>
895
+ </tr>
896
+
897
+ <tr>
898
+ <td align="left"><code>visible</code></td>
899
+ <td align="left" style="width: 25%;">
900
+
901
+ ```python
902
+ bool
903
+ ```
904
+
905
+ </td>
906
+ <td align="left"><code>True</code></td>
907
+ <td align="left">If False, component will be hidden.</td>
908
+ </tr>
909
+
910
+ <tr>
911
+ <td align="left"><code>elem_id</code></td>
912
+ <td align="left" style="width: 25%;">
913
+
914
+ ```python
915
+ str | None
916
+ ```
917
+
918
+ </td>
919
+ <td align="left"><code>None</code></td>
920
+ <td align="left">An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles.</td>
921
+ </tr>
922
+
923
+ <tr>
924
+ <td align="left"><code>elem_classes</code></td>
925
+ <td align="left" style="width: 25%;">
926
+
927
+ ```python
928
+ list[str] | str | None
929
+ ```
930
+
931
+ </td>
932
+ <td align="left"><code>None</code></td>
933
+ <td align="left">An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.</td>
934
+ </tr>
935
+
936
+ <tr>
937
+ <td align="left"><code>render</code></td>
938
+ <td align="left" style="width: 25%;">
939
+
940
+ ```python
941
+ bool
942
+ ```
943
+
944
+ </td>
945
+ <td align="left"><code>True</code></td>
946
+ <td align="left">If False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later.</td>
947
+ </tr>
948
+
949
+ <tr>
950
+ <td align="left"><code>key</code></td>
951
+ <td align="left" style="width: 25%;">
952
+
953
+ ```python
954
+ int | str | tuple[int | str, ...] | None
955
+ ```
956
+
957
+ </td>
958
+ <td align="left"><code>None</code></td>
959
+ <td align="left">in a gr.render, Components with the same key across re-renders are treated as the same component, not a new component. Properties set in 'preserved_by_key' are not reset across a re-render.</td>
960
+ </tr>
961
+
962
+ <tr>
963
+ <td align="left"><code>preserved_by_key</code></td>
964
+ <td align="left" style="width: 25%;">
965
+
966
+ ```python
967
+ list[str] | str | None
968
+ ```
969
+
970
+ </td>
971
+ <td align="left"><code>"value"</code></td>
972
+ <td align="left">A list of parameters from this component's constructor. Inside a gr.render() function, if a component is re-rendered with the same key, these (and only these) parameters will be preserved in the UI (if they have been changed by the user or an event listener) instead of re-rendered based on the values provided during constructor.</td>
973
+ </tr>
974
+
975
+ <tr>
976
+ <td align="left"><code>width</code></td>
977
+ <td align="left" style="width: 25%;">
978
+
979
+ ```python
980
+ int | None
981
+ ```
982
+
983
+ </td>
984
+ <td align="left"><code>1000</code></td>
985
+ <td align="left">width of the piano roll component in pixels.</td>
986
+ </tr>
987
+
988
+ <tr>
989
+ <td align="left"><code>height</code></td>
990
+ <td align="left" style="width: 25%;">
991
+
992
+ ```python
993
+ int | None
994
+ ```
995
+
996
+ </td>
997
+ <td align="left"><code>600</code></td>
998
+ <td align="left">height of the piano roll component in pixels.</td>
999
+ </tr>
1000
+ </tbody></table>
1001
+
1002
+
1003
+ ### Events
1004
+
1005
+ | name | description |
1006
+ |:-----|:------------|
1007
+ | `change` | Triggered when the value of the PianoRoll changes either because of user input (e.g. a user types in a textbox) OR because of a function update (e.g. an image receives a value from the output of an event trigger). See `.input()` for a listener that is only triggered by user input. |
1008
+ | `input` | This listener is triggered when the user changes the value of the PianoRoll. |
1009
+ | `play` | This listener is triggered when the user plays the media in the PianoRoll. |
1010
+ | `pause` | This listener is triggered when the media in the PianoRoll stops for any reason. |
1011
+ | `stop` | This listener is triggered when the user reaches the end of the media playing in the PianoRoll. |
1012
+ | `clear` | This listener is triggered when the user clears the PianoRoll using the clear button for the component. |
1013
+
1014
+
1015
+