This commit delivers three key improvements focused on user experience and bug fixing.
Browse files1. **Add Rich Info Text to 8-bit Synthesizer UI**
2. **Detect and Log Stereo MIDI Information**
3. **Fix Stereo Loss in "Convert to Solo Piano":**
- Resolved a critical bug where enabling "Convert to Solo Piano" would collapse stereo MIDI files into mono.
- app.py +115 -15
- src/TMIDIX.py +26 -12
app.py
CHANGED
|
@@ -529,6 +529,44 @@ def merge_midis(midi_path_left, midi_path_right, output_path):
|
|
| 529 |
return None
|
| 530 |
|
| 531 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 532 |
# =================================================================================================
|
| 533 |
# === Stage 1: Audio to MIDI Transcription Functions ===
|
| 534 |
# =================================================================================================
|
|
@@ -1194,6 +1232,10 @@ def run_single_file_pipeline(input_file_path: str, timestamp: str, params: AppPa
|
|
| 1194 |
# For MIDI files, we start at 0% and directly proceed to the rendering steps.
|
| 1195 |
update_progress(0, "MIDI file detected, skipping transcription...")
|
| 1196 |
print("MIDI file detected. Skipping transcription. Proceeding directly to rendering.")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1197 |
midi_path_for_rendering = input_file_path
|
| 1198 |
else:
|
| 1199 |
temp_dir = "output/temp_transcribe" # Define temp_dir early for the fallback
|
|
@@ -2349,23 +2391,81 @@ if __name__ == "__main__":
|
|
| 2349 |
# Define the 8-bit UI components in one place for easy reference
|
| 2350 |
gr.Markdown("### 8-bit Synthesizer Settings")
|
| 2351 |
with gr.Accordion("8-bit Synthesizer Settings", open=True, visible=False) as synth_8bit_settings:
|
| 2352 |
-
s8bit_preset_selector = gr.Dropdown(
|
| 2353 |
-
|
| 2354 |
-
|
| 2355 |
-
|
| 2356 |
-
|
| 2357 |
-
|
| 2358 |
-
|
| 2359 |
-
|
| 2360 |
-
|
| 2361 |
-
|
| 2362 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2363 |
# --- New accordion for advanced effects ---
|
| 2364 |
with gr.Accordion("Advanced Synthesis & FX", open=False):
|
| 2365 |
-
s8bit_noise_level = gr.Slider(
|
| 2366 |
-
|
| 2367 |
-
|
| 2368 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2369 |
|
| 2370 |
# Create a dictionary mapping key names to the actual Gradio components
|
| 2371 |
ui_component_map = locals()
|
|
|
|
| 529 |
return None
|
| 530 |
|
| 531 |
|
| 532 |
+
def is_stereo_midi(midi_path: str) -> bool:
|
| 533 |
+
"""
|
| 534 |
+
Checks if a MIDI file contains the specific stereo panning control changes
|
| 535 |
+
(hard left and hard right) created by the merge_midis function.
|
| 536 |
+
|
| 537 |
+
Args:
|
| 538 |
+
midi_path (str): The file path to the MIDI file.
|
| 539 |
+
|
| 540 |
+
Returns:
|
| 541 |
+
bool: True if both hard-left (0) and hard-right (127) pan controls are found, False otherwise.
|
| 542 |
+
"""
|
| 543 |
+
try:
|
| 544 |
+
midi_data = pretty_midi.PrettyMIDI(midi_path)
|
| 545 |
+
|
| 546 |
+
found_left_pan = False
|
| 547 |
+
found_right_pan = False
|
| 548 |
+
|
| 549 |
+
for instrument in midi_data.instruments:
|
| 550 |
+
for control_change in instrument.control_changes:
|
| 551 |
+
# MIDI Controller Number 10 is for Panning.
|
| 552 |
+
if control_change.number == 10:
|
| 553 |
+
if control_change.value == 0:
|
| 554 |
+
found_left_pan = True
|
| 555 |
+
elif control_change.value == 127:
|
| 556 |
+
found_right_pan = True
|
| 557 |
+
|
| 558 |
+
# Optimization: If we've already found both, no need to check further.
|
| 559 |
+
if found_left_pan and found_right_pan:
|
| 560 |
+
return True
|
| 561 |
+
|
| 562 |
+
return found_left_pan and found_right_pan
|
| 563 |
+
|
| 564 |
+
except Exception as e:
|
| 565 |
+
# If the MIDI file is invalid or another error occurs, assume it's not our special stereo format.
|
| 566 |
+
print(f"Could not analyze MIDI for stereo info: {e}")
|
| 567 |
+
return False
|
| 568 |
+
|
| 569 |
+
|
| 570 |
# =================================================================================================
|
| 571 |
# === Stage 1: Audio to MIDI Transcription Functions ===
|
| 572 |
# =================================================================================================
|
|
|
|
| 1232 |
# For MIDI files, we start at 0% and directly proceed to the rendering steps.
|
| 1233 |
update_progress(0, "MIDI file detected, skipping transcription...")
|
| 1234 |
print("MIDI file detected. Skipping transcription. Proceeding directly to rendering.")
|
| 1235 |
+
|
| 1236 |
+
if is_stereo_midi(input_file_path):
|
| 1237 |
+
print("\nINFO: Stereo pan information (Left/Right) detected in the input MIDI. It will be rendered in stereo.\n")
|
| 1238 |
+
|
| 1239 |
midi_path_for_rendering = input_file_path
|
| 1240 |
else:
|
| 1241 |
temp_dir = "output/temp_transcribe" # Define temp_dir early for the fallback
|
|
|
|
| 2391 |
# Define the 8-bit UI components in one place for easy reference
|
| 2392 |
gr.Markdown("### 8-bit Synthesizer Settings")
|
| 2393 |
with gr.Accordion("8-bit Synthesizer Settings", open=True, visible=False) as synth_8bit_settings:
|
| 2394 |
+
s8bit_preset_selector = gr.Dropdown(
|
| 2395 |
+
choices=["Custom", "Auto-Recommend (Analyze MIDI)"] + list(S8BIT_PRESETS.keys()),
|
| 2396 |
+
value="Custom",
|
| 2397 |
+
label="Style Preset",
|
| 2398 |
+
info="Select a preset to auto-fill the settings below. Choose 'Custom' for manual control or 'Auto-Recommend' to analyze the MIDI.\nFor reference and entertainment only. These presets are not guaranteed to be perfectly accurate."
|
| 2399 |
+
)
|
| 2400 |
+
s8bit_waveform_type = gr.Dropdown(
|
| 2401 |
+
['Square', 'Sawtooth', 'Triangle'],
|
| 2402 |
+
value='Square',
|
| 2403 |
+
label="Waveform Type",
|
| 2404 |
+
info="The fundamental timbre of the sound. Square is bright and hollow (classic NES), Sawtooth is aggressive and buzzy, Triangle is soft and flute-like."
|
| 2405 |
+
)
|
| 2406 |
+
s8bit_pulse_width = gr.Slider(
|
| 2407 |
+
0.01, 0.99, value=0.5, step=0.01,
|
| 2408 |
+
label="Pulse Width (Square Wave Only)",
|
| 2409 |
+
info="Changes the character of the Square wave. Low values (~0.1) are thin and nasal, while mid values (~0.5) are full and round."
|
| 2410 |
+
)
|
| 2411 |
+
s8bit_envelope_type = gr.Dropdown(
|
| 2412 |
+
['Plucky (AD Envelope)', 'Sustained (Full Decay)'],
|
| 2413 |
+
value='Plucky (AD Envelope)',
|
| 2414 |
+
label="Envelope Type",
|
| 2415 |
+
info="Shapes the volume of each note. 'Plucky' is a short, percussive sound. 'Sustained' holds the note for its full duration."
|
| 2416 |
+
)
|
| 2417 |
+
s8bit_decay_time_s = gr.Slider(
|
| 2418 |
+
0.01, 1.0, value=0.1, step=0.01,
|
| 2419 |
+
label="Decay Time (s)",
|
| 2420 |
+
info="For the 'Plucky' envelope, this is the time it takes for a note to fade to silence. Low values are short and staccato; high values are longer and more resonant."
|
| 2421 |
+
)
|
| 2422 |
+
s8bit_vibrato_rate = gr.Slider(
|
| 2423 |
+
0, 20, value=5,
|
| 2424 |
+
label="Vibrato Rate (Hz)",
|
| 2425 |
+
info="The SPEED of the pitch wobble. Low values create a slow, gentle waver. High values create a fast, frantic buzz."
|
| 2426 |
+
)
|
| 2427 |
+
s8bit_vibrato_depth = gr.Slider(
|
| 2428 |
+
0, 50, value=0,
|
| 2429 |
+
label="Vibrato Depth (Hz)",
|
| 2430 |
+
info="The INTENSITY of the pitch wobble. Low values are subtle or off. High values create a dramatic, siren-like pitch bend."
|
| 2431 |
+
)
|
| 2432 |
+
s8bit_bass_boost_level = gr.Slider(
|
| 2433 |
+
0.0, 1.0, value=0.0, step=0.05,
|
| 2434 |
+
label="Bass Boost Level",
|
| 2435 |
+
info="Mixes in a sub-octave (a square wave one octave lower). Low values have no effect; high values add significant weight and power."
|
| 2436 |
+
)
|
| 2437 |
+
s8bit_smooth_notes_level = gr.Slider(
|
| 2438 |
+
0.0, 1.0, value=0.0, step=0.05,
|
| 2439 |
+
label="Smooth Notes Level",
|
| 2440 |
+
info="Applies a tiny fade-in/out to reduce clicking. Low values (or 0) give a hard, abrupt attack. High values give a softer, cleaner onset."
|
| 2441 |
+
)
|
| 2442 |
+
s8bit_continuous_vibrato_level = gr.Slider(
|
| 2443 |
+
0.0, 1.0, value=0.0, step=0.05,
|
| 2444 |
+
label="Continuous Vibrato Level",
|
| 2445 |
+
info="Controls vibrato continuity across notes. Low values (0) reset vibrato on each note (bouncy). High values (1) create a smooth, connected 'singing' vibrato."
|
| 2446 |
+
)
|
| 2447 |
# --- New accordion for advanced effects ---
|
| 2448 |
with gr.Accordion("Advanced Synthesis & FX", open=False):
|
| 2449 |
+
s8bit_noise_level = gr.Slider(
|
| 2450 |
+
0.0, 1.0, value=0.0, step=0.05,
|
| 2451 |
+
label="Noise Level",
|
| 2452 |
+
info="Mixes in white noise with the main waveform. Low values are clean; high values add 'grit', 'air', or a hissing quality, useful for percussion."
|
| 2453 |
+
)
|
| 2454 |
+
s8bit_distortion_level = gr.Slider(
|
| 2455 |
+
0.0, 0.9, value=0.0, step=0.05,
|
| 2456 |
+
label="Distortion Level",
|
| 2457 |
+
info="Applies wave-shaping to make the sound harsher. Low values are clean; high values create a crushed, 'fuzzy', and aggressive tone."
|
| 2458 |
+
)
|
| 2459 |
+
s8bit_fm_modulation_depth = gr.Slider(
|
| 2460 |
+
0.0, 1.0, value=0.0, step=0.05,
|
| 2461 |
+
label="FM Depth",
|
| 2462 |
+
info="Frequency Modulation intensity. At low values, there is no effect. At high values, it creates complex, metallic, or bell-like tones."
|
| 2463 |
+
)
|
| 2464 |
+
s8bit_fm_modulation_rate = gr.Slider(
|
| 2465 |
+
0.0, 500.0, value=0.0, step=1.0,
|
| 2466 |
+
label="FM Rate",
|
| 2467 |
+
info="Frequency Modulation speed. Low values create a slow 'wobble'. High values create fast modulation, resulting in bright, dissonant harmonics."
|
| 2468 |
+
)
|
| 2469 |
|
| 2470 |
# Create a dictionary mapping key names to the actual Gradio components
|
| 2471 |
ui_component_map = locals()
|
src/TMIDIX.py
CHANGED
|
@@ -6048,32 +6048,46 @@ def solo_piano_escore_notes(escore_notes,
|
|
| 6048 |
patches_index=6,
|
| 6049 |
keep_drums=False,
|
| 6050 |
):
|
| 6051 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6052 |
cscore = chordify_score([1000, escore_notes])
|
| 6053 |
|
| 6054 |
sp_escore_notes = []
|
| 6055 |
|
| 6056 |
for c in cscore:
|
| 6057 |
-
|
| 6058 |
-
|
| 6059 |
chord = []
|
| 6060 |
|
| 6061 |
for cc in c:
|
| 6062 |
|
| 6063 |
-
if cc[channels_index] != 9:
|
| 6064 |
-
|
| 6065 |
-
|
| 6066 |
-
|
| 6067 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6068 |
|
| 6069 |
chord.append(cc)
|
| 6070 |
-
|
| 6071 |
|
| 6072 |
-
else:
|
| 6073 |
if keep_drums:
|
| 6074 |
-
|
|
|
|
|
|
|
| 6075 |
chord.append(cc)
|
| 6076 |
-
|
| 6077 |
|
| 6078 |
sp_escore_notes.append(chord)
|
| 6079 |
|
|
|
|
| 6048 |
patches_index=6,
|
| 6049 |
keep_drums=False,
|
| 6050 |
):
|
| 6051 |
+
"""
|
| 6052 |
+
A modified version of TMIDIX.solo_piano_escore_notes that preserves the
|
| 6053 |
+
original MIDI channel of each note. This allows stereo panning information,
|
| 6054 |
+
which is often channel-dependent, to be maintained during the conversion
|
| 6055 |
+
to a solo piano performance.
|
| 6056 |
+
"""
|
| 6057 |
cscore = chordify_score([1000, escore_notes])
|
| 6058 |
|
| 6059 |
sp_escore_notes = []
|
| 6060 |
|
| 6061 |
for c in cscore:
|
| 6062 |
+
# --- Use a set to store (pitch, channel) tuples for uniqueness ---
|
| 6063 |
+
seen_notes = set()
|
| 6064 |
chord = []
|
| 6065 |
|
| 6066 |
for cc in c:
|
| 6067 |
|
| 6068 |
+
if cc[channels_index] != 9: # If not a drum channel
|
| 6069 |
+
# Create a unique identifier for each note using both pitch and channel
|
| 6070 |
+
note_id = (cc[pitches_index], cc[channels_index])
|
| 6071 |
+
|
| 6072 |
+
# Check if this specific pitch-channel combination has been seen
|
| 6073 |
+
if note_id not in seen_notes:
|
| 6074 |
+
# The original function forced the channel to 0, destroying stereo separation.
|
| 6075 |
+
# We comment out that line and ONLY change the instrument patch.
|
| 6076 |
+
# cc[channels_index] = 0 <-- THIS LINE IS REMOVED
|
| 6077 |
+
|
| 6078 |
+
# Force the instrument patch to 0 (Acoustic Grand Piano)
|
| 6079 |
+
cc[patches_index] = 0 # Set patch to Grand Piano
|
| 6080 |
|
| 6081 |
chord.append(cc)
|
| 6082 |
+
seen_notes.add(note_id) # Add the unique ID to the set
|
| 6083 |
|
| 6084 |
+
else: # If it is a drum channel
|
| 6085 |
if keep_drums:
|
| 6086 |
+
# Apply the same logic for drums to be safe
|
| 6087 |
+
drum_id = (cc[pitches_index] + 128, cc[channels_index])
|
| 6088 |
+
if drum_id not in seen_notes:
|
| 6089 |
chord.append(cc)
|
| 6090 |
+
seen_notes.add(drum_id)
|
| 6091 |
|
| 6092 |
sp_escore_notes.append(chord)
|
| 6093 |
|