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.
- gradio_pianoroll-0.0.2/PKG-INFO +1015 -0
- gradio_pianoroll-0.0.2/README.md +990 -0
- gradio_pianoroll-0.0.2/TIMING_CONVERSIONS.md +327 -0
- gradio_pianoroll-0.0.2/backend/gradio_pianoroll/pianoroll.py +559 -0
- gradio_pianoroll-0.0.2/backend/gradio_pianoroll/pianoroll.pyi +729 -0
- {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/backend/gradio_pianoroll/templates/component/index.js +7593 -7077
- {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/backend/gradio_pianoroll/templates/component/style.css +1 -1
- gradio_pianoroll-0.0.2/demo/SYNTHESIZER_GUIDE.md +115 -0
- gradio_pianoroll-0.0.2/demo/app.py +673 -0
- gradio_pianoroll-0.0.2/demo/requirements.txt +3 -0
- gradio_pianoroll-0.0.2/demo/space.py +772 -0
- gradio_pianoroll-0.0.2/frontend/Index.svelte +201 -0
- {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/frontend/components/GridComponent.svelte +293 -218
- gradio_pianoroll-0.0.2/frontend/components/PianoRoll.svelte +818 -0
- {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/frontend/components/PlayheadComponent.svelte +3 -3
- {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/frontend/components/TimeLineComponent.svelte +18 -0
- {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/frontend/components/WaveformComponent.svelte +452 -304
- {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/frontend/utils/audioEngine.ts +139 -64
- gradio_pianoroll-0.0.2/frontend/utils/flicks.ts +194 -0
- {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/pyproject.toml +2 -2
- gradio_pianoroll-0.0.1/PKG-INFO +0 -337
- gradio_pianoroll-0.0.1/README.md +0 -312
- gradio_pianoroll-0.0.1/backend/gradio_pianoroll/pianoroll.py +0 -201
- gradio_pianoroll-0.0.1/backend/gradio_pianoroll/pianoroll.pyi +0 -292
- gradio_pianoroll-0.0.1/demo/app.py +0 -51
- gradio_pianoroll-0.0.1/demo/requirements.txt +0 -1
- gradio_pianoroll-0.0.1/demo/space.py +0 -150
- gradio_pianoroll-0.0.1/frontend/Index.svelte +0 -134
- gradio_pianoroll-0.0.1/frontend/components/PianoRoll.svelte +0 -374
- gradio_pianoroll-0.0.1/frontend/utils/flicks.ts +0 -76
- {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/.gitignore +0 -0
- {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/backend/gradio_pianoroll/__init__.py +0 -0
- {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/backend/gradio_pianoroll/templates/example/index.js +0 -0
- {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/backend/gradio_pianoroll/templates/example/style.css +0 -0
- {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/demo/__init__.py +0 -0
- {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/demo/css.css +0 -0
- {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/frontend/Example.svelte +0 -0
- {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/frontend/components/DebugComponent.svelte +0 -0
- {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/frontend/components/KeyboardComponent.svelte +0 -0
- {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/frontend/components/Toolbar.svelte +0 -0
- {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/frontend/gradio.config.js +0 -0
- {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/frontend/package-lock.json +0 -0
- {gradio_pianoroll-0.0.1 → gradio_pianoroll-0.0.2}/frontend/package.json +0 -0
- {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
|
+
|