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 |
|