Upload 2 files
Browse files- TMIDIX.py +129 -43
- midi_to_colab_audio.py +679 -223
TMIDIX.py
CHANGED
@@ -51,7 +51,7 @@ r'''############################################################################
|
|
51 |
|
52 |
###################################################################################
|
53 |
|
54 |
-
__version__ = "25.7.
|
55 |
|
56 |
print('=' * 70)
|
57 |
print('TMIDIX Python module')
|
@@ -3724,19 +3724,52 @@ def validate_pitches(chord, channel_to_check = 0, return_sorted = True):
|
|
3724 |
chord.sort(key = lambda x: x[4], reverse=True)
|
3725 |
return chord
|
3726 |
|
3727 |
-
def adjust_score_velocities(score,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3728 |
|
3729 |
-
|
3730 |
-
|
3731 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3732 |
|
3733 |
-
max_channel_velocity = max([c[5] for c in score])
|
3734 |
-
if max_channel_velocity < min_velocity:
|
3735 |
-
factor = max_velocity / min_velocity
|
3736 |
else:
|
3737 |
-
|
3738 |
-
|
3739 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3740 |
|
3741 |
def chordify_score(score,
|
3742 |
return_choridfied_score=True,
|
@@ -5029,54 +5062,101 @@ def patch_list_from_enhanced_score_notes(enhanced_score_notes,
|
|
5029 |
|
5030 |
###################################################################################
|
5031 |
|
5032 |
-
def patch_enhanced_score_notes(
|
5033 |
-
|
5034 |
-
|
5035 |
-
|
5036 |
-
|
|
|
|
|
5037 |
|
5038 |
-
#===========================================================================
|
|
|
|
|
|
|
|
|
5039 |
|
5040 |
enhanced_score_notes_with_patch_changes = []
|
5041 |
|
5042 |
patches = [-1] * 16
|
5043 |
|
|
|
|
|
|
|
5044 |
overflow_idx = -1
|
5045 |
|
5046 |
for idx, e in enumerate(enhanced_score_notes):
|
5047 |
-
|
5048 |
-
|
5049 |
-
|
5050 |
-
|
5051 |
-
|
5052 |
-
|
5053 |
-
|
5054 |
-
e[3] =
|
5055 |
-
|
5056 |
-
|
5057 |
-
|
5058 |
-
|
5059 |
-
|
5060 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
5061 |
|
5062 |
-
|
5063 |
|
5064 |
#===========================================================================
|
5065 |
|
5066 |
overflow_patches = []
|
|
|
|
|
|
|
|
|
|
|
5067 |
|
5068 |
if overflow_idx != -1:
|
5069 |
-
|
5070 |
-
|
5071 |
-
|
5072 |
-
|
5073 |
-
|
5074 |
-
|
5075 |
-
|
5076 |
-
|
5077 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
5078 |
|
5079 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
5080 |
|
5081 |
#===========================================================================
|
5082 |
|
@@ -5086,9 +5166,13 @@ def patch_enhanced_score_notes(enhanced_score_notes,
|
|
5086 |
|
5087 |
#===========================================================================
|
5088 |
|
|
|
|
|
|
|
|
|
5089 |
if verbose:
|
5090 |
print('=' * 70)
|
5091 |
-
print('
|
5092 |
print('=' * 70)
|
5093 |
for c, p in enumerate(patches):
|
5094 |
print('Cha', str(c).zfill(2), '---', str(p).zfill(3), Number2patch[p])
|
@@ -5101,6 +5185,8 @@ def patch_enhanced_score_notes(enhanced_score_notes,
|
|
5101 |
print(str(p).zfill(3), Number2patch[p])
|
5102 |
print('=' * 70)
|
5103 |
|
|
|
|
|
5104 |
return enhanced_score_notes_with_patch_changes, patches, overflow_patches
|
5105 |
|
5106 |
###################################################################################
|
|
|
51 |
|
52 |
###################################################################################
|
53 |
|
54 |
+
__version__ = "25.7.8"
|
55 |
|
56 |
print('=' * 70)
|
57 |
print('TMIDIX Python module')
|
|
|
3724 |
chord.sort(key = lambda x: x[4], reverse=True)
|
3725 |
return chord
|
3726 |
|
3727 |
+
def adjust_score_velocities(score,
|
3728 |
+
max_velocity,
|
3729 |
+
adj_per_channel=False,
|
3730 |
+
adj_in_place=True
|
3731 |
+
):
|
3732 |
+
|
3733 |
+
if adj_in_place:
|
3734 |
+
buf = score
|
3735 |
+
|
3736 |
+
else:
|
3737 |
+
buf = copy.deepcopy(score)
|
3738 |
+
|
3739 |
+
notes = [evt for evt in buf if evt[0] == 'note']
|
3740 |
+
|
3741 |
+
if not notes:
|
3742 |
+
return buf
|
3743 |
|
3744 |
+
if adj_per_channel:
|
3745 |
+
channel_max = {}
|
3746 |
+
|
3747 |
+
for _, _, _, ch, _, vel, _ in notes:
|
3748 |
+
channel_max[ch] = max(channel_max.get(ch, 0), vel)
|
3749 |
+
|
3750 |
+
channel_factor = {
|
3751 |
+
ch: (max_velocity / vmax if vmax > 0 else 1.0)
|
3752 |
+
for ch, vmax in channel_max.items()
|
3753 |
+
}
|
3754 |
+
|
3755 |
+
for evt in buf:
|
3756 |
+
if evt[0] == 'note':
|
3757 |
+
ch = evt[3]
|
3758 |
+
factor = channel_factor.get(ch, 1.0)
|
3759 |
+
new_vel = int(evt[5] * factor)
|
3760 |
+
evt[5] = max(1, min(127, new_vel))
|
3761 |
|
|
|
|
|
|
|
3762 |
else:
|
3763 |
+
global_max = max(vel for _, _, _, _, _, vel, _ in notes)
|
3764 |
+
factor = max_velocity / global_max if global_max > 0 else 1.0
|
3765 |
+
|
3766 |
+
for evt in buf:
|
3767 |
+
if evt[0] == 'note':
|
3768 |
+
new_vel = int(evt[5] * factor)
|
3769 |
+
evt[5] = max(1, min(127, new_vel))
|
3770 |
+
|
3771 |
+
if not adj_in_place:
|
3772 |
+
return buf
|
3773 |
|
3774 |
def chordify_score(score,
|
3775 |
return_choridfied_score=True,
|
|
|
5062 |
|
5063 |
###################################################################################
|
5064 |
|
5065 |
+
def patch_enhanced_score_notes(escore_notes,
|
5066 |
+
default_patch=0,
|
5067 |
+
reserved_patch=-1,
|
5068 |
+
reserved_patch_channel=-1,
|
5069 |
+
drums_patch=9,
|
5070 |
+
verbose=False
|
5071 |
+
):
|
5072 |
|
5073 |
+
#===========================================================================
|
5074 |
+
|
5075 |
+
enhanced_score_notes = copy.deepcopy(escore_notes)
|
5076 |
+
|
5077 |
+
#===========================================================================
|
5078 |
|
5079 |
enhanced_score_notes_with_patch_changes = []
|
5080 |
|
5081 |
patches = [-1] * 16
|
5082 |
|
5083 |
+
if -1 < reserved_patch < 128 and -1 < reserved_patch_channel < 128:
|
5084 |
+
patches[reserved_patch_channel] = reserved_patch
|
5085 |
+
|
5086 |
overflow_idx = -1
|
5087 |
|
5088 |
for idx, e in enumerate(enhanced_score_notes):
|
5089 |
+
if e[0] == 'note':
|
5090 |
+
if e[3] != 9:
|
5091 |
+
if -1 < reserved_patch < 128 and -1 < reserved_patch_channel < 128:
|
5092 |
+
if e[6] == reserved_patch:
|
5093 |
+
e[3] = reserved_patch_channel
|
5094 |
+
|
5095 |
+
if patches[e[3]] == -1:
|
5096 |
+
patches[e[3]] = e[6]
|
5097 |
+
|
5098 |
+
else:
|
5099 |
+
if patches[e[3]] != e[6]:
|
5100 |
+
if e[6] in patches:
|
5101 |
+
e[3] = patches.index(e[6])
|
5102 |
+
|
5103 |
+
else:
|
5104 |
+
if -1 in patches:
|
5105 |
+
patches[patches.index(-1)] = e[6]
|
5106 |
+
|
5107 |
+
else:
|
5108 |
+
overflow_idx = idx
|
5109 |
+
break
|
5110 |
|
5111 |
+
enhanced_score_notes_with_patch_changes.append(e)
|
5112 |
|
5113 |
#===========================================================================
|
5114 |
|
5115 |
overflow_patches = []
|
5116 |
+
overflow_channels = [-1] * 16
|
5117 |
+
overflow_channels[9] = drums_patch
|
5118 |
+
|
5119 |
+
if -1 < reserved_patch < 128 and -1 < reserved_patch_channel < 128:
|
5120 |
+
overflow_channels[reserved_patch_channel] = reserved_patch
|
5121 |
|
5122 |
if overflow_idx != -1:
|
5123 |
+
for idx, e in enumerate(enhanced_score_notes[overflow_idx:]):
|
5124 |
+
if e[0] == 'note':
|
5125 |
+
if e[3] != 9:
|
5126 |
+
if e[6] not in overflow_channels:
|
5127 |
+
|
5128 |
+
if -1 in overflow_channels:
|
5129 |
+
free_chan = overflow_channels.index(-1)
|
5130 |
+
overflow_channels[free_chan] = e[6]
|
5131 |
+
e[3] = free_chan
|
5132 |
+
|
5133 |
+
enhanced_score_notes_with_patch_changes.append(['patch_change', e[1], e[3], e[6]])
|
5134 |
+
|
5135 |
+
overflow_patches.append(e[6])
|
5136 |
+
|
5137 |
+
else:
|
5138 |
+
overflow_channels = [-1] * 16
|
5139 |
+
overflow_channels[9] = drums_patch
|
5140 |
+
|
5141 |
+
if -1 < reserved_patch < 128 and -1 < reserved_patch_channel < 128:
|
5142 |
+
overflow_channels[reserved_patch_channel] = reserved_patch
|
5143 |
+
e[3] = reserved_patch_channel
|
5144 |
|
5145 |
+
if e[6] != reserved_patch:
|
5146 |
+
|
5147 |
+
free_chan = overflow_channels.index(-1)
|
5148 |
+
e[3] = free_chan
|
5149 |
+
|
5150 |
+
overflow_channels[e[3]] = e[6]
|
5151 |
+
|
5152 |
+
enhanced_score_notes_with_patch_changes.append(['patch_change', e[1], e[3], e[6]])
|
5153 |
+
|
5154 |
+
overflow_patches.append(e[6])
|
5155 |
+
|
5156 |
+
else:
|
5157 |
+
e[3] = overflow_channels.index(e[6])
|
5158 |
+
|
5159 |
+
enhanced_score_notes_with_patch_changes.append(e)
|
5160 |
|
5161 |
#===========================================================================
|
5162 |
|
|
|
5166 |
|
5167 |
#===========================================================================
|
5168 |
|
5169 |
+
overflow_patches = ordered_set(overflow_patches)
|
5170 |
+
|
5171 |
+
#===========================================================================
|
5172 |
+
|
5173 |
if verbose:
|
5174 |
print('=' * 70)
|
5175 |
+
print('Main composition patches')
|
5176 |
print('=' * 70)
|
5177 |
for c, p in enumerate(patches):
|
5178 |
print('Cha', str(c).zfill(2), '---', str(p).zfill(3), Number2patch[p])
|
|
|
5185 |
print(str(p).zfill(3), Number2patch[p])
|
5186 |
print('=' * 70)
|
5187 |
|
5188 |
+
#===========================================================================
|
5189 |
+
|
5190 |
return enhanced_score_notes_with_patch_changes, patches, overflow_patches
|
5191 |
|
5192 |
###################################################################################
|
midi_to_colab_audio.py
CHANGED
@@ -5,14 +5,14 @@ r'''#===========================================================================
|
|
5 |
# Converts any MIDI file to raw audio which is compatible
|
6 |
# with Google Colab or HUgging Face Gradio
|
7 |
#
|
8 |
-
# Version
|
9 |
#
|
10 |
-
# Includes full source code of MIDI
|
11 |
#
|
12 |
-
# Original source code for all modules was retrieved on
|
13 |
#
|
14 |
# Project Los Angeles
|
15 |
-
# Tegridy Code
|
16 |
#
|
17 |
#===================================================================================================================
|
18 |
#
|
@@ -1773,7 +1773,7 @@ def _encode(events_lol, unknown_callback=None, never_add_eot=False,
|
|
1773 |
|
1774 |
Python bindings for FluidSynth
|
1775 |
|
1776 |
-
Copyright 2008, Nathan Whitehead <[email protected]>
|
1777 |
|
1778 |
|
1779 |
Released under the LGPL
|
@@ -1790,27 +1790,67 @@ def _encode(events_lol, unknown_callback=None, never_add_eot=False,
|
|
1790 |
================================================================================
|
1791 |
"""
|
1792 |
|
1793 |
-
from ctypes import *
|
1794 |
-
from ctypes.util import find_library
|
1795 |
import os
|
1796 |
-
|
1797 |
-
|
1798 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1799 |
|
1800 |
# DLL search method changed in Python 3.8
|
1801 |
# https://docs.python.org/3/library/os.html#os.add_dll_directory
|
1802 |
-
if hasattr(os, 'add_dll_directory'):
|
1803 |
os.add_dll_directory(os.getcwd())
|
|
|
|
|
|
|
1804 |
|
1805 |
-
|
1806 |
-
|
1807 |
-
|
1808 |
-
|
1809 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1810 |
|
1811 |
-
if lib is None:
|
1812 |
raise ImportError("Couldn't find the FluidSynth library.")
|
1813 |
|
|
|
|
|
1814 |
# Dynamically link the FluidSynth library
|
1815 |
# Architecture (32-/64-bit) must match your Python version
|
1816 |
_fl = CDLL(lib)
|
@@ -1829,7 +1869,7 @@ def cfunc(name, result, *args):
|
|
1829 |
return None
|
1830 |
|
1831 |
# Bump this up when changing the interface for users
|
1832 |
-
api_version = '1.3.
|
1833 |
|
1834 |
# Function prototypes for C versions of functions
|
1835 |
|
@@ -1843,10 +1883,7 @@ fluid_version = cfunc('fluid_version', c_void_p,
|
|
1843 |
|
1844 |
majver = c_int()
|
1845 |
fluid_version(majver, c_int(), c_int())
|
1846 |
-
if majver.value > 1
|
1847 |
-
FLUIDSETTING_EXISTS = FLUID_OK
|
1848 |
-
else:
|
1849 |
-
FLUIDSETTING_EXISTS = 1
|
1850 |
|
1851 |
# fluid settings
|
1852 |
new_fluid_settings = cfunc('new_fluid_settings', c_void_p)
|
@@ -2086,9 +2123,18 @@ fluid_synth_set_chorus_level = cfunc('fluid_synth_set_chorus_level', c_int,
|
|
2086 |
('synth', c_void_p, 1),
|
2087 |
('level', c_double, 1))
|
2088 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2089 |
fluid_synth_set_chorus_type = cfunc('fluid_synth_set_chorus_type', c_int,
|
2090 |
('synth', c_void_p, 1),
|
2091 |
('type', c_int, 1))
|
|
|
2092 |
fluid_synth_get_reverb_roomsize = cfunc('fluid_synth_get_reverb_roomsize', c_double,
|
2093 |
('synth', c_void_p, 1))
|
2094 |
|
@@ -2220,6 +2266,77 @@ fluid_midi_event_get_value = cfunc('fluid_midi_event_get_value', c_int,
|
|
2220 |
fluid_midi_event_get_velocity = cfunc('fluid_midi_event_get_velocity', c_int,
|
2221 |
('evt', c_void_p, 1))
|
2222 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2223 |
# fluid_player_status returned by fluid_player_get_status()
|
2224 |
FLUID_PLAYER_READY = 0
|
2225 |
FLUID_PLAYER_PLAYING = 1
|
@@ -2281,6 +2398,9 @@ new_fluid_midi_driver = cfunc('new_fluid_midi_driver', c_void_p,
|
|
2281 |
('handler', CFUNCTYPE(c_int, c_void_p, c_void_p), 1),
|
2282 |
('event_handler_data', c_void_p, 1))
|
2283 |
|
|
|
|
|
|
|
2284 |
|
2285 |
# fluid midi router rule
|
2286 |
class fluid_midi_router_t(Structure):
|
@@ -2342,6 +2462,16 @@ fluid_midi_router_add_rule = cfunc('fluid_midi_router_add_rule', c_int,
|
|
2342 |
('rule', c_void_p, 1),
|
2343 |
('type', c_int, 1))
|
2344 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2345 |
# fluidsynth 2.x
|
2346 |
new_fluid_cmd_handler=cfunc('new_fluid_cmd_handler', c_void_p,
|
2347 |
('synth', c_void_p, 1),
|
@@ -2416,6 +2546,7 @@ class Synth:
|
|
2416 |
self.audio_driver = None
|
2417 |
self.midi_driver = None
|
2418 |
self.router = None
|
|
|
2419 |
def setting(self, opt, val):
|
2420 |
"""change an arbitrary synth setting, type-smart"""
|
2421 |
if isinstance(val, (str, bytes)):
|
@@ -2451,11 +2582,11 @@ class Synth:
|
|
2451 |
see http://www.fluidsynth.org/api/fluidsettings.xml for allowed values and defaults by platform
|
2452 |
"""
|
2453 |
driver = driver or self.get_setting('audio.driver')
|
2454 |
-
device = device or self.get_setting('audio
|
2455 |
midi_driver = midi_driver or self.get_setting('midi.driver')
|
2456 |
|
2457 |
self.setting('audio.driver', driver)
|
2458 |
-
self.setting('audio
|
2459 |
self.audio_driver = new_fluid_audio_driver(self.settings, self.synth)
|
2460 |
self.setting('midi.driver', midi_driver)
|
2461 |
self.router = new_fluid_midi_router(self.settings, fluid_synth_handle_midi_event, self.synth)
|
@@ -2463,7 +2594,7 @@ class Synth:
|
|
2463 |
new_fluid_cmd_handler(self.synth, self.router)
|
2464 |
else:
|
2465 |
fluid_synth_set_midi_router(self.synth, self.router)
|
2466 |
-
if midi_router
|
2467 |
self.midi_driver = new_fluid_midi_driver(self.settings, fluid_midi_router_handle_midi_event, self.router)
|
2468 |
self.custom_router_callback = None
|
2469 |
else: ## Supply an external MIDI event handler
|
@@ -2474,6 +2605,8 @@ class Synth:
|
|
2474 |
def delete(self):
|
2475 |
if self.audio_driver:
|
2476 |
delete_fluid_audio_driver(self.audio_driver)
|
|
|
|
|
2477 |
delete_fluid_synth(self.synth)
|
2478 |
delete_fluid_settings(self.settings)
|
2479 |
def sfload(self, filename, update_midi_preset=0):
|
@@ -2518,8 +2651,7 @@ class Synth:
|
|
2518 |
return None
|
2519 |
return fluid_preset_get_name(preset).decode('ascii')
|
2520 |
else:
|
2521 |
-
|
2522 |
-
return presetname
|
2523 |
def router_clear(self):
|
2524 |
if self.router is not None:
|
2525 |
fluid_midi_router_clear_rules(self.router)
|
@@ -2570,16 +2702,16 @@ class Synth:
|
|
2570 |
if fluid_synth_set_reverb is not None:
|
2571 |
return fluid_synth_set_reverb(self.synth, roomsize, damping, width, level)
|
2572 |
else:
|
2573 |
-
|
2574 |
if roomsize>=0:
|
2575 |
-
|
2576 |
if damping>=0:
|
2577 |
-
|
2578 |
if width>=0:
|
2579 |
-
|
2580 |
if level>=0:
|
2581 |
-
|
2582 |
-
return fluid_synth_set_reverb_full(self.synth,
|
2583 |
def set_chorus(self, nr=-1, level=-1.0, speed=-1.0, depth=-1.0, type=-1):
|
2584 |
"""
|
2585 |
nr Chorus voice count (0-99, CPU time consumption proportional to this value)
|
@@ -2632,17 +2764,17 @@ class Synth:
|
|
2632 |
if fluid_synth_set_chorus_level is not None:
|
2633 |
return fluid_synth_set_chorus_level(self.synth, level)
|
2634 |
else:
|
2635 |
-
return self.set_chorus(
|
2636 |
def set_chorus_speed(self, speed):
|
2637 |
if fluid_synth_set_chorus_speed is not None:
|
2638 |
return fluid_synth_set_chorus_speed(self.synth, speed)
|
2639 |
else:
|
2640 |
return self.set_chorus(speed=speed)
|
2641 |
-
def set_chorus_depth(self,
|
2642 |
if fluid_synth_set_chorus_depth is not None:
|
2643 |
-
return fluid_synth_set_chorus_depth(self.synth,
|
2644 |
else:
|
2645 |
-
return self.set_chorus(depth=
|
2646 |
def set_chorus_type(self, type):
|
2647 |
if fluid_synth_set_chorus_type is not None:
|
2648 |
return fluid_synth_set_chorus_type(self.synth, type)
|
@@ -2694,10 +2826,10 @@ class Synth:
|
|
2694 |
A pitch bend value of 0 is no pitch change from default.
|
2695 |
A value of -2048 is 1 semitone down.
|
2696 |
A value of 2048 is 1 semitone up.
|
2697 |
-
Maximum values are -8192 to +
|
2698 |
|
2699 |
"""
|
2700 |
-
return fluid_synth_pitch_bend(self.synth, chan, val + 8192)
|
2701 |
def cc(self, chan, ctrl, val):
|
2702 |
"""Send control change value
|
2703 |
|
@@ -2747,8 +2879,15 @@ class Synth:
|
|
2747 |
|
2748 |
"""
|
2749 |
return fluid_synth_write_s16_stereo(self.synth, len)
|
2750 |
-
def tuning_dump(self, bank, prog
|
2751 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2752 |
|
2753 |
def midi_event_get_type(self, event):
|
2754 |
return fluid_midi_event_get_type(event)
|
@@ -2767,17 +2906,20 @@ class Synth:
|
|
2767 |
|
2768 |
def play_midi_file(self, filename):
|
2769 |
self.player = new_fluid_player(self.synth)
|
2770 |
-
if self.player
|
2771 |
-
|
|
|
2772 |
fluid_player_set_playback_callback(self.player, self.custom_router_callback, self.synth)
|
2773 |
status = fluid_player_add(self.player, filename.encode())
|
2774 |
-
if status == FLUID_FAILED:
|
|
|
2775 |
status = fluid_player_play(self.player)
|
2776 |
return status
|
2777 |
|
2778 |
def play_midi_stop(self):
|
2779 |
status = fluid_player_stop(self.player)
|
2780 |
-
if status == FLUID_FAILED:
|
|
|
2781 |
status = fluid_player_seek(self.player, 0)
|
2782 |
delete_fluid_player(self.player)
|
2783 |
return status
|
@@ -2785,7 +2927,151 @@ class Synth:
|
|
2785 |
def player_set_tempo(self, tempo_type, tempo):
|
2786 |
return fluid_player_set_tempo(self.player, tempo_type, tempo)
|
2787 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2788 |
|
|
|
|
|
|
|
|
|
|
|
2789 |
|
2790 |
class Sequencer:
|
2791 |
def __init__(self, time_scale=1000, use_system_timer=True):
|
@@ -2802,14 +3088,14 @@ class Sequencer:
|
|
2802 |
def register_fluidsynth(self, synth):
|
2803 |
response = fluid_sequencer_register_fluidsynth(self.sequencer, synth.synth)
|
2804 |
if response == FLUID_FAILED:
|
2805 |
-
raise
|
2806 |
return response
|
2807 |
|
2808 |
def register_client(self, name, callback, data=None):
|
2809 |
c_callback = CFUNCTYPE(None, c_uint, c_void_p, c_void_p, c_void_p)(callback)
|
2810 |
response = fluid_sequencer_register_client(self.sequencer, name.encode(), c_callback, data)
|
2811 |
if response == FLUID_FAILED:
|
2812 |
-
raise
|
2813 |
|
2814 |
# store in a list to prevent garbage collection
|
2815 |
self.client_callbacks.append(c_callback)
|
@@ -2849,7 +3135,7 @@ class Sequencer:
|
|
2849 |
def _schedule_event(self, evt, time, absolute=True):
|
2850 |
response = fluid_sequencer_send_at(self.sequencer, evt, time, absolute)
|
2851 |
if response == FLUID_FAILED:
|
2852 |
-
raise
|
2853 |
|
2854 |
def get_tick(self):
|
2855 |
return fluid_sequencer_get_tick(self.sequencer)
|
@@ -2868,123 +3154,248 @@ def raw_audio_string(data):
|
|
2868 |
|
2869 |
"""
|
2870 |
import numpy
|
2871 |
-
return (data.astype(numpy.int16)).
|
2872 |
|
2873 |
#===============================================================================
|
2874 |
|
2875 |
import numpy as np
|
2876 |
import wave
|
2877 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2878 |
def midi_opus_to_colab_audio(midi_opus,
|
2879 |
soundfont_path='/usr/share/sounds/sf2/FluidR3_GM.sf2',
|
2880 |
sample_rate=16000, # 44100
|
2881 |
-
|
2882 |
trim_silence=True,
|
2883 |
silence_threshold=0.1,
|
2884 |
output_for_gradio=False,
|
2885 |
write_audio_to_WAV=''
|
2886 |
):
|
2887 |
|
2888 |
-
def normalize_volume(matrix, factor=10):
|
2889 |
-
norm = np.linalg.norm(matrix)
|
2890 |
-
matrix = matrix/norm # normalized matrix
|
2891 |
-
mult_matrix = matrix * factor
|
2892 |
-
final_matrix = np.clip(mult_matrix, -1.0, 1.0)
|
2893 |
-
return final_matrix
|
2894 |
-
|
2895 |
if midi_opus[1]:
|
2896 |
|
2897 |
-
|
2898 |
-
|
2899 |
-
|
2900 |
-
|
2901 |
-
|
2902 |
-
|
2903 |
-
|
2904 |
-
|
2905 |
-
|
2906 |
-
|
2907 |
-
|
2908 |
-
|
2909 |
-
|
2910 |
-
|
2911 |
-
|
2912 |
-
|
2913 |
-
|
2914 |
-
|
2915 |
-
|
2916 |
-
|
2917 |
-
|
2918 |
-
|
2919 |
-
|
2920 |
-
|
2921 |
-
|
2922 |
-
|
2923 |
-
|
2924 |
-
|
2925 |
-
|
2926 |
-
|
2927 |
-
|
2928 |
-
|
2929 |
-
|
2930 |
-
|
2931 |
-
|
2932 |
-
|
2933 |
-
|
2934 |
-
|
2935 |
-
|
2936 |
-
|
2937 |
-
|
2938 |
-
|
2939 |
-
|
2940 |
-
|
2941 |
-
|
2942 |
-
|
2943 |
-
|
2944 |
-
|
2945 |
-
|
2946 |
-
|
2947 |
-
|
2948 |
-
|
2949 |
-
|
2950 |
-
|
2951 |
-
|
2952 |
-
|
2953 |
-
|
2954 |
-
|
2955 |
-
|
2956 |
-
|
2957 |
-
|
2958 |
-
|
2959 |
-
|
2960 |
-
|
2961 |
-
|
2962 |
-
|
2963 |
-
|
2964 |
-
|
2965 |
-
|
2966 |
-
|
2967 |
-
|
2968 |
-
|
2969 |
-
|
2970 |
-
|
2971 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2972 |
|
2973 |
else:
|
2974 |
return None
|
2975 |
|
2976 |
-
|
2977 |
-
|
2978 |
-
|
2979 |
-
|
|
|
|
|
2980 |
trim_silence=True,
|
2981 |
silence_threshold=0.1,
|
2982 |
output_for_gradio=False,
|
2983 |
write_audio_to_WAV=False
|
2984 |
-
|
2985 |
-
|
2986 |
-
'''
|
2987 |
-
|
2988 |
Returns raw audio to pass to IPython.disaply.Audio func
|
2989 |
|
2990 |
Example usage:
|
@@ -2992,99 +3403,144 @@ def midi_to_colab_audio(midi_file,
|
|
2992 |
from IPython.display import Audio
|
2993 |
|
2994 |
display(Audio(raw_audio, rate=16000, normalize=False))
|
2995 |
-
|
2996 |
-
'''
|
2997 |
-
|
2998 |
-
def normalize_volume(matrix, factor=10):
|
2999 |
-
norm = np.linalg.norm(matrix)
|
3000 |
-
matrix = matrix/norm # normalized matrix
|
3001 |
-
mult_matrix = matrix * factor
|
3002 |
-
final_matrix = np.clip(mult_matrix, -1.0, 1.0)
|
3003 |
-
return final_matrix
|
3004 |
-
|
3005 |
-
midi_opus = midi2opus(open(midi_file, 'rb').read())
|
3006 |
|
3007 |
-
|
|
|
|
|
|
|
3008 |
|
3009 |
-
|
3010 |
-
|
3011 |
-
|
3012 |
-
|
3013 |
-
|
3014 |
-
|
3015 |
-
|
3016 |
-
|
3017 |
-
|
3018 |
-
|
3019 |
-
|
3020 |
-
|
3021 |
-
|
3022 |
-
|
3023 |
-
|
3024 |
-
|
3025 |
-
|
3026 |
-
|
3027 |
-
|
3028 |
-
|
3029 |
-
|
3030 |
-
|
3031 |
-
|
3032 |
-
|
3033 |
-
|
3034 |
-
|
3035 |
-
|
3036 |
-
|
3037 |
-
|
3038 |
-
|
3039 |
-
|
3040 |
-
|
3041 |
-
|
3042 |
-
|
3043 |
-
|
3044 |
-
|
3045 |
-
|
3046 |
-
|
3047 |
-
|
3048 |
-
|
3049 |
-
|
3050 |
-
|
3051 |
-
|
3052 |
-
|
3053 |
-
|
3054 |
-
|
3055 |
-
|
3056 |
-
|
3057 |
-
|
3058 |
-
|
3059 |
-
|
3060 |
-
|
3061 |
-
|
3062 |
-
|
3063 |
-
|
3064 |
-
|
3065 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3066 |
|
3067 |
-
|
|
|
|
|
3068 |
|
3069 |
-
|
|
|
3070 |
|
3071 |
-
|
|
|
|
|
|
|
|
|
3072 |
|
3073 |
-
|
|
|
|
|
|
|
|
|
|
|
3074 |
|
3075 |
-
|
|
|
|
|
3076 |
|
3077 |
-
|
|
|
|
|
3078 |
|
3079 |
-
|
|
|
|
|
|
|
|
|
3080 |
wf.setframerate(sample_rate)
|
3081 |
wf.setsampwidth(2)
|
3082 |
-
wf.setnchannels(
|
3083 |
-
wf.writeframes(
|
|
|
|
|
3084 |
|
3085 |
-
return raw_audio
|
3086 |
-
|
3087 |
-
else:
|
3088 |
-
return None
|
3089 |
-
|
3090 |
#===================================================================================================================
|
|
|
5 |
# Converts any MIDI file to raw audio which is compatible
|
6 |
# with Google Colab or HUgging Face Gradio
|
7 |
#
|
8 |
+
# Version 2.0
|
9 |
#
|
10 |
+
# Includes full source code of MIDI and pyfluidsynth
|
11 |
#
|
12 |
+
# Original source code for all modules was retrieved on 07/31/2025
|
13 |
#
|
14 |
# Project Los Angeles
|
15 |
+
# Tegridy Code 2025
|
16 |
#
|
17 |
#===================================================================================================================
|
18 |
#
|
|
|
1773 |
|
1774 |
Python bindings for FluidSynth
|
1775 |
|
1776 |
+
Copyright 2008--2024, Nathan Whitehead <[email protected]> and others.
|
1777 |
|
1778 |
|
1779 |
Released under the LGPL
|
|
|
1790 |
================================================================================
|
1791 |
"""
|
1792 |
|
|
|
|
|
1793 |
import os
|
1794 |
+
from ctypes import (
|
1795 |
+
CDLL,
|
1796 |
+
CFUNCTYPE,
|
1797 |
+
POINTER,
|
1798 |
+
Structure,
|
1799 |
+
byref,
|
1800 |
+
c_char,
|
1801 |
+
c_char_p,
|
1802 |
+
c_double,
|
1803 |
+
c_float,
|
1804 |
+
c_int,
|
1805 |
+
c_short,
|
1806 |
+
c_uint,
|
1807 |
+
c_void_p,
|
1808 |
+
create_string_buffer,
|
1809 |
+
)
|
1810 |
+
from ctypes.util import find_library
|
1811 |
|
1812 |
# DLL search method changed in Python 3.8
|
1813 |
# https://docs.python.org/3/library/os.html#os.add_dll_directory
|
1814 |
+
if hasattr(os, 'add_dll_directory'): # Python 3.8+ on Windows only
|
1815 |
os.add_dll_directory(os.getcwd())
|
1816 |
+
os.add_dll_directory('C:\\tools\\fluidsynth\\bin')
|
1817 |
+
# Workaround bug in find_library, it doesn't recognize add_dll_directory
|
1818 |
+
os.environ['PATH'] += ';C:\\tools\\fluidsynth\\bin'
|
1819 |
|
1820 |
+
# A function to find the FluidSynth library
|
1821 |
+
# (mostly needed for Windows distributions of libfluidsynth supplied with QSynth)
|
1822 |
+
def find_libfluidsynth(debug_print: bool = False) -> str:
|
1823 |
+
r"""
|
1824 |
+
macOS X64:
|
1825 |
+
* 'fluidsynth' was found at /usr/local/opt/fluid-synth/lib/libfluidsynth.dylib.
|
1826 |
+
macOS ARM64:
|
1827 |
+
* 'fluidsynth' was found at /opt/homebrew/opt/fluid-synth/lib/libfluidsynth.dylib.
|
1828 |
+
Ubuntu X86:
|
1829 |
+
* 'fluidsynth' was found at libfluidsynth.so.3.
|
1830 |
+
Windows X86:
|
1831 |
+
* 'libfluidsynth-3' was found at C:\tools\fluidsynth\bin\libfluidsynth-3.dll. --or--
|
1832 |
+
* 'fluidsynth-3' was found as C:\tools\fluidsynth\bin\fluidsynth-3.dll. >= v2.4.5
|
1833 |
+
* https://github.com/FluidSynth/fluidsynth/issues/1543
|
1834 |
+
"""
|
1835 |
+
libs = "fluidsynth fluidsynth-3 libfluidsynth libfluidsynth-3 libfluidsynth-2 libfluidsynth-1"
|
1836 |
+
for lib_name in libs.split():
|
1837 |
+
lib = find_library(lib_name)
|
1838 |
+
if lib:
|
1839 |
+
if debug_print:
|
1840 |
+
print(f"'{lib_name}' was found at {lib}.")
|
1841 |
+
return lib
|
1842 |
+
|
1843 |
+
# On macOS on Apple silicon, non-Homebrew Python distributions fail to locate
|
1844 |
+
# homebrew-installed instances of FluidSynth. This workaround addresses this.
|
1845 |
+
if homebrew_prefix := os.getenv("HOMEBREW_PREFIX"):
|
1846 |
+
lib = os.path.join(homebrew_prefix, "lib", "libfluidsynth.dylib")
|
1847 |
+
if os.path.exists(lib):
|
1848 |
+
return lib
|
1849 |
|
|
|
1850 |
raise ImportError("Couldn't find the FluidSynth library.")
|
1851 |
|
1852 |
+
lib = find_libfluidsynth()
|
1853 |
+
|
1854 |
# Dynamically link the FluidSynth library
|
1855 |
# Architecture (32-/64-bit) must match your Python version
|
1856 |
_fl = CDLL(lib)
|
|
|
1869 |
return None
|
1870 |
|
1871 |
# Bump this up when changing the interface for users
|
1872 |
+
api_version = '1.3.5'
|
1873 |
|
1874 |
# Function prototypes for C versions of functions
|
1875 |
|
|
|
1883 |
|
1884 |
majver = c_int()
|
1885 |
fluid_version(majver, c_int(), c_int())
|
1886 |
+
FLUIDSETTING_EXISTS = FLUID_OK if majver.value > 1 else 1
|
|
|
|
|
|
|
1887 |
|
1888 |
# fluid settings
|
1889 |
new_fluid_settings = cfunc('new_fluid_settings', c_void_p)
|
|
|
2123 |
('synth', c_void_p, 1),
|
2124 |
('level', c_double, 1))
|
2125 |
|
2126 |
+
fluid_synth_set_chorus_speed = cfunc('fluid_synth_set_chorus_speed', c_int,
|
2127 |
+
('synth', c_void_p, 1),
|
2128 |
+
('speed', c_double, 1))
|
2129 |
+
|
2130 |
+
fluid_synth_set_chorus_depth = cfunc('fluid_synth_set_chorus_depth', c_int,
|
2131 |
+
('synth', c_void_p, 1),
|
2132 |
+
('depth_ms', c_double, 1))
|
2133 |
+
|
2134 |
fluid_synth_set_chorus_type = cfunc('fluid_synth_set_chorus_type', c_int,
|
2135 |
('synth', c_void_p, 1),
|
2136 |
('type', c_int, 1))
|
2137 |
+
|
2138 |
fluid_synth_get_reverb_roomsize = cfunc('fluid_synth_get_reverb_roomsize', c_double,
|
2139 |
('synth', c_void_p, 1))
|
2140 |
|
|
|
2266 |
fluid_midi_event_get_velocity = cfunc('fluid_midi_event_get_velocity', c_int,
|
2267 |
('evt', c_void_p, 1))
|
2268 |
|
2269 |
+
# fluid modulator
|
2270 |
+
new_fluid_mod = cfunc("new_fluid_mod", c_void_p)
|
2271 |
+
|
2272 |
+
delete_fluid_mod = cfunc("delete_fluid_mod", c_void_p, ("mod", c_void_p, 1))
|
2273 |
+
|
2274 |
+
fluid_mod_clone = cfunc(
|
2275 |
+
"fluid_mod_clone", c_void_p, ("mod", c_void_p, 1), ("src", c_void_p, 1),
|
2276 |
+
)
|
2277 |
+
|
2278 |
+
fluid_mod_get_amount = cfunc("fluid_mod_get_amount", c_void_p, ("mod", c_void_p, 1))
|
2279 |
+
|
2280 |
+
fluid_mod_get_dest = cfunc("fluid_mod_get_dest", c_void_p, ("mod", c_void_p, 1))
|
2281 |
+
|
2282 |
+
fluid_mod_get_flags1 = cfunc("fluid_mod_get_flags1", c_void_p, ("mod", c_void_p, 1))
|
2283 |
+
|
2284 |
+
fluid_mod_get_flags2 = cfunc("fluid_mod_get_flags2", c_void_p, ("mod", c_void_p, 1))
|
2285 |
+
|
2286 |
+
fluid_mod_get_source1 = cfunc("fluid_mod_get_source1", c_void_p, ("mod", c_void_p, 1))
|
2287 |
+
|
2288 |
+
fluid_mod_get_source2 = cfunc("fluid_mod_get_source2", c_void_p, ("mod", c_void_p, 1))
|
2289 |
+
|
2290 |
+
fluid_mod_get_transform = cfunc(
|
2291 |
+
"fluid_mod_get_transform", c_void_p, ("mod", c_void_p, 1),
|
2292 |
+
)
|
2293 |
+
|
2294 |
+
fluid_mod_has_dest = cfunc(
|
2295 |
+
"fluid_mod_has_dest", c_void_p, ("mod", c_void_p, 1), ("gen", c_uint, 1),
|
2296 |
+
)
|
2297 |
+
|
2298 |
+
fluid_mod_has_source = cfunc(
|
2299 |
+
"fluid_mod_has_dest",
|
2300 |
+
c_void_p,
|
2301 |
+
("mod", c_void_p, 1),
|
2302 |
+
("cc", c_uint, 1),
|
2303 |
+
("ctrl", c_uint, 1),
|
2304 |
+
)
|
2305 |
+
|
2306 |
+
fluid_mod_set_amount = cfunc(
|
2307 |
+
"fluid_mod_set_amount", c_void_p, ("mod", c_void_p, 1), ("amount", c_double, 1),
|
2308 |
+
)
|
2309 |
+
|
2310 |
+
fluid_mod_set_dest = cfunc(
|
2311 |
+
"fluid_mod_set_dest", c_void_p, ("mod", c_void_p, 1), ("dst", c_int, 1),
|
2312 |
+
)
|
2313 |
+
|
2314 |
+
fluid_mod_set_source1 = cfunc(
|
2315 |
+
"fluid_mod_set_source1",
|
2316 |
+
c_void_p,
|
2317 |
+
("mod", c_void_p, 1),
|
2318 |
+
("src", c_int, 1),
|
2319 |
+
("flags", c_int, 1),
|
2320 |
+
)
|
2321 |
+
|
2322 |
+
fluid_mod_set_source2 = cfunc(
|
2323 |
+
"fluid_mod_set_source2",
|
2324 |
+
c_void_p,
|
2325 |
+
("mod", c_void_p, 1),
|
2326 |
+
("src", c_int, 1),
|
2327 |
+
("flags", c_int, 1),
|
2328 |
+
)
|
2329 |
+
|
2330 |
+
fluid_mod_set_transform = cfunc(
|
2331 |
+
"fluid_mod_set_transform", c_void_p, ("mod", c_void_p, 1), ("type", c_int, 1),
|
2332 |
+
)
|
2333 |
+
|
2334 |
+
fluid_mod_sizeof = cfunc("fluid_mod_sizeof", c_void_p)
|
2335 |
+
|
2336 |
+
fluid_mod_test_identity = cfunc(
|
2337 |
+
"fluid_mod_test_identity", c_void_p, ("mod1", c_void_p, 1), ("mod2", c_void_p, 1),
|
2338 |
+
)
|
2339 |
+
|
2340 |
# fluid_player_status returned by fluid_player_get_status()
|
2341 |
FLUID_PLAYER_READY = 0
|
2342 |
FLUID_PLAYER_PLAYING = 1
|
|
|
2398 |
('handler', CFUNCTYPE(c_int, c_void_p, c_void_p), 1),
|
2399 |
('event_handler_data', c_void_p, 1))
|
2400 |
|
2401 |
+
delete_fluid_midi_driver = cfunc('delete_fluid_midi_driver', None,
|
2402 |
+
('driver', c_void_p, 1))
|
2403 |
+
|
2404 |
|
2405 |
# fluid midi router rule
|
2406 |
class fluid_midi_router_t(Structure):
|
|
|
2462 |
('rule', c_void_p, 1),
|
2463 |
('type', c_int, 1))
|
2464 |
|
2465 |
+
# fluid file renderer
|
2466 |
+
new_fluid_file_renderer = cfunc('new_fluid_file_renderer', c_void_p,
|
2467 |
+
('synth', c_void_p, 1))
|
2468 |
+
|
2469 |
+
delete_fluid_file_renderer = cfunc('delete_fluid_file_renderer', None,
|
2470 |
+
('renderer', c_void_p, 1))
|
2471 |
+
|
2472 |
+
fluid_file_renderer_process_block = cfunc('fluid_file_renderer_process_block', c_int,
|
2473 |
+
('render', c_void_p, 1))
|
2474 |
+
|
2475 |
# fluidsynth 2.x
|
2476 |
new_fluid_cmd_handler=cfunc('new_fluid_cmd_handler', c_void_p,
|
2477 |
('synth', c_void_p, 1),
|
|
|
2546 |
self.audio_driver = None
|
2547 |
self.midi_driver = None
|
2548 |
self.router = None
|
2549 |
+
self.custom_router_callback = None
|
2550 |
def setting(self, opt, val):
|
2551 |
"""change an arbitrary synth setting, type-smart"""
|
2552 |
if isinstance(val, (str, bytes)):
|
|
|
2582 |
see http://www.fluidsynth.org/api/fluidsettings.xml for allowed values and defaults by platform
|
2583 |
"""
|
2584 |
driver = driver or self.get_setting('audio.driver')
|
2585 |
+
device = device or self.get_setting(f'audio.{driver}.device')
|
2586 |
midi_driver = midi_driver or self.get_setting('midi.driver')
|
2587 |
|
2588 |
self.setting('audio.driver', driver)
|
2589 |
+
self.setting(f'audio.{driver}.device', device)
|
2590 |
self.audio_driver = new_fluid_audio_driver(self.settings, self.synth)
|
2591 |
self.setting('midi.driver', midi_driver)
|
2592 |
self.router = new_fluid_midi_router(self.settings, fluid_synth_handle_midi_event, self.synth)
|
|
|
2594 |
new_fluid_cmd_handler(self.synth, self.router)
|
2595 |
else:
|
2596 |
fluid_synth_set_midi_router(self.synth, self.router)
|
2597 |
+
if midi_router is None: ## Use fluidsynth to create a MIDI event handler
|
2598 |
self.midi_driver = new_fluid_midi_driver(self.settings, fluid_midi_router_handle_midi_event, self.router)
|
2599 |
self.custom_router_callback = None
|
2600 |
else: ## Supply an external MIDI event handler
|
|
|
2605 |
def delete(self):
|
2606 |
if self.audio_driver:
|
2607 |
delete_fluid_audio_driver(self.audio_driver)
|
2608 |
+
if self.midi_driver:
|
2609 |
+
delete_fluid_midi_driver(self.midi_driver)
|
2610 |
delete_fluid_synth(self.synth)
|
2611 |
delete_fluid_settings(self.settings)
|
2612 |
def sfload(self, filename, update_midi_preset=0):
|
|
|
2651 |
return None
|
2652 |
return fluid_preset_get_name(preset).decode('ascii')
|
2653 |
else:
|
2654 |
+
return None
|
|
|
2655 |
def router_clear(self):
|
2656 |
if self.router is not None:
|
2657 |
fluid_midi_router_clear_rules(self.router)
|
|
|
2702 |
if fluid_synth_set_reverb is not None:
|
2703 |
return fluid_synth_set_reverb(self.synth, roomsize, damping, width, level)
|
2704 |
else:
|
2705 |
+
flags=0
|
2706 |
if roomsize>=0:
|
2707 |
+
flags+=0b0001
|
2708 |
if damping>=0:
|
2709 |
+
flags+=0b0010
|
2710 |
if width>=0:
|
2711 |
+
flags+=0b0100
|
2712 |
if level>=0:
|
2713 |
+
flags+=0b1000
|
2714 |
+
return fluid_synth_set_reverb_full(self.synth, flags, roomsize, damping, width, level)
|
2715 |
def set_chorus(self, nr=-1, level=-1.0, speed=-1.0, depth=-1.0, type=-1):
|
2716 |
"""
|
2717 |
nr Chorus voice count (0-99, CPU time consumption proportional to this value)
|
|
|
2764 |
if fluid_synth_set_chorus_level is not None:
|
2765 |
return fluid_synth_set_chorus_level(self.synth, level)
|
2766 |
else:
|
2767 |
+
return self.set_chorus(level=level)
|
2768 |
def set_chorus_speed(self, speed):
|
2769 |
if fluid_synth_set_chorus_speed is not None:
|
2770 |
return fluid_synth_set_chorus_speed(self.synth, speed)
|
2771 |
else:
|
2772 |
return self.set_chorus(speed=speed)
|
2773 |
+
def set_chorus_depth(self, depth_ms):
|
2774 |
if fluid_synth_set_chorus_depth is not None:
|
2775 |
+
return fluid_synth_set_chorus_depth(self.synth, depth_ms)
|
2776 |
else:
|
2777 |
+
return self.set_chorus(depth=depth_ms)
|
2778 |
def set_chorus_type(self, type):
|
2779 |
if fluid_synth_set_chorus_type is not None:
|
2780 |
return fluid_synth_set_chorus_type(self.synth, type)
|
|
|
2826 |
A pitch bend value of 0 is no pitch change from default.
|
2827 |
A value of -2048 is 1 semitone down.
|
2828 |
A value of 2048 is 1 semitone up.
|
2829 |
+
Maximum values are -8192 to +8191 (transposing by 4 semitones).
|
2830 |
|
2831 |
"""
|
2832 |
+
return fluid_synth_pitch_bend(self.synth, chan, max(0, min(val + 8192, 16383)))
|
2833 |
def cc(self, chan, ctrl, val):
|
2834 |
"""Send control change value
|
2835 |
|
|
|
2879 |
|
2880 |
"""
|
2881 |
return fluid_synth_write_s16_stereo(self.synth, len)
|
2882 |
+
def tuning_dump(self, bank, prog):
|
2883 |
+
"""Get tuning information for given bank and preset
|
2884 |
+
|
2885 |
+
Return value is an array of length 128 with tuning factors for each MIDI note.
|
2886 |
+
Tuning factor of 0.0 in each position is standard tuning. Measured in cents.
|
2887 |
+
"""
|
2888 |
+
pitch = (c_double * 128)()
|
2889 |
+
fluid_synth_tuning_dump(self.synth, bank, prog, None, 0, pitch)
|
2890 |
+
return pitch[:]
|
2891 |
|
2892 |
def midi_event_get_type(self, event):
|
2893 |
return fluid_midi_event_get_type(event)
|
|
|
2906 |
|
2907 |
def play_midi_file(self, filename):
|
2908 |
self.player = new_fluid_player(self.synth)
|
2909 |
+
if self.player is None:
|
2910 |
+
return FLUID_FAILED
|
2911 |
+
if self.custom_router_callback is not None:
|
2912 |
fluid_player_set_playback_callback(self.player, self.custom_router_callback, self.synth)
|
2913 |
status = fluid_player_add(self.player, filename.encode())
|
2914 |
+
if status == FLUID_FAILED:
|
2915 |
+
return status
|
2916 |
status = fluid_player_play(self.player)
|
2917 |
return status
|
2918 |
|
2919 |
def play_midi_stop(self):
|
2920 |
status = fluid_player_stop(self.player)
|
2921 |
+
if status == FLUID_FAILED:
|
2922 |
+
return status
|
2923 |
status = fluid_player_seek(self.player, 0)
|
2924 |
delete_fluid_player(self.player)
|
2925 |
return status
|
|
|
2927 |
def player_set_tempo(self, tempo_type, tempo):
|
2928 |
return fluid_player_set_tempo(self.player, tempo_type, tempo)
|
2929 |
|
2930 |
+
def midi2audio(self, midifile, audiofile = "output.wav"):
|
2931 |
+
"""Convert a midi file to an audio file"""
|
2932 |
+
self.setting("audio.file.name", audiofile)
|
2933 |
+
player = new_fluid_player(self.synth)
|
2934 |
+
fluid_player_add(player, midifile.encode())
|
2935 |
+
fluid_player_play(player)
|
2936 |
+
renderer = new_fluid_file_renderer(self.synth)
|
2937 |
+
while fluid_player_get_status(player) == FLUID_PLAYER_PLAYING:
|
2938 |
+
if fluid_file_renderer_process_block(renderer) != FLUID_OK:
|
2939 |
+
break
|
2940 |
+
delete_fluid_file_renderer(renderer)
|
2941 |
+
delete_fluid_player(player)
|
2942 |
+
|
2943 |
+
# flag values
|
2944 |
+
FLUID_MOD_POSITIVE = 0
|
2945 |
+
FLUID_MOD_NEGATIVE = 1
|
2946 |
+
FLUID_MOD_UNIPOLAR = 0
|
2947 |
+
FLUID_MOD_BIPOLAR = 2
|
2948 |
+
FLUID_MOD_LINEAR = 0
|
2949 |
+
FLUID_MOD_CONCAVE = 4
|
2950 |
+
FLUID_MOD_CONVEX = 8
|
2951 |
+
FLUID_MOD_SWITCH = 12
|
2952 |
+
FLUID_MOD_GC = 0
|
2953 |
+
FLUID_MOD_CC = 16
|
2954 |
+
FLUID_MOD_SIN = 0x80
|
2955 |
+
|
2956 |
+
# src values
|
2957 |
+
FLUID_MOD_NONE = 0
|
2958 |
+
FLUID_MOD_VELOCITY = 2
|
2959 |
+
FLUID_MOD_KEY = 3
|
2960 |
+
FLUID_MOD_KEYPRESSURE = 10
|
2961 |
+
FLUID_MOD_CHANNELPRESSURE = 13
|
2962 |
+
FLUID_MOD_PITCHWHEEL = 14
|
2963 |
+
FLUID_MOD_PITCHWHEELSENS = 16
|
2964 |
+
|
2965 |
+
# Transforms
|
2966 |
+
FLUID_MOD_TRANSFORM_LINEAR = 0
|
2967 |
+
FLUID_MOD_TRANSFORM_ABS = 2
|
2968 |
+
|
2969 |
+
class Modulator:
|
2970 |
+
def __init__(self):
|
2971 |
+
"""Create new modulator object"""
|
2972 |
+
self.mod = new_fluid_mod()
|
2973 |
+
|
2974 |
+
def clone(self, src):
|
2975 |
+
response = fluid_mod_clone(self.mod, src)
|
2976 |
+
if response == FLUID_FAILED:
|
2977 |
+
raise Exception("Modulation clone failed")
|
2978 |
+
return response
|
2979 |
+
|
2980 |
+
def get_amount(self):
|
2981 |
+
response = fluid_mod_get_amount(self.mod)
|
2982 |
+
if response == FLUID_FAILED:
|
2983 |
+
raise Exception("Modulation amount get failed")
|
2984 |
+
return response
|
2985 |
+
|
2986 |
+
def get_dest(self):
|
2987 |
+
response = fluid_mod_get_dest(self.mod)
|
2988 |
+
if response == FLUID_FAILED:
|
2989 |
+
raise Exception("Modulation destination get failed")
|
2990 |
+
return response
|
2991 |
+
|
2992 |
+
def get_flags1(self):
|
2993 |
+
response = fluid_mod_get_flags1(self.mod)
|
2994 |
+
if response == FLUID_FAILED:
|
2995 |
+
raise Exception("Modulation flags1 get failed")
|
2996 |
+
return response
|
2997 |
+
|
2998 |
+
def get_flags2(self):
|
2999 |
+
response = fluid_mod_get_flags2(self.mod)
|
3000 |
+
if response == FLUID_FAILED:
|
3001 |
+
raise Exception("Modulation flags2 get failed")
|
3002 |
+
return response
|
3003 |
+
|
3004 |
+
def get_source1(self):
|
3005 |
+
response = fluid_mod_get_source1(self.mod)
|
3006 |
+
if response == FLUID_FAILED:
|
3007 |
+
raise Exception("Modulation source1 get failed")
|
3008 |
+
return response
|
3009 |
+
|
3010 |
+
def get_source2(self):
|
3011 |
+
response = fluid_mod_get_source2(self.mod)
|
3012 |
+
if response == FLUID_FAILED:
|
3013 |
+
raise Exception("Modulation source2 get failed")
|
3014 |
+
return response
|
3015 |
+
|
3016 |
+
def get_transform(self):
|
3017 |
+
response = fluid_mod_get_transform(self.mod)
|
3018 |
+
if response == FLUID_FAILED:
|
3019 |
+
raise Exception("Modulation transform get failed")
|
3020 |
+
return response
|
3021 |
+
|
3022 |
+
def has_dest(self, gen):
|
3023 |
+
response = fluid_mod_has_dest(self.mod, gen)
|
3024 |
+
if response == FLUID_FAILED:
|
3025 |
+
raise Exception("Modulation has destination check failed")
|
3026 |
+
return response
|
3027 |
+
|
3028 |
+
def has_source(self, cc, ctrl):
|
3029 |
+
response = fluid_mod_has_source(self.mod, cc, ctrl)
|
3030 |
+
if response == FLUID_FAILED:
|
3031 |
+
raise Exception("Modulation has source check failed")
|
3032 |
+
return response
|
3033 |
+
|
3034 |
+
def set_amount(self, amount):
|
3035 |
+
response = fluid_mod_set_amount(self.mod, amount)
|
3036 |
+
if response == FLUID_FAILED:
|
3037 |
+
raise Exception("Modulation set amount failed")
|
3038 |
+
return response
|
3039 |
+
|
3040 |
+
def set_dest(self, dest):
|
3041 |
+
response = fluid_mod_set_dest(self.mod, dest)
|
3042 |
+
if response == FLUID_FAILED:
|
3043 |
+
raise Exception("Modulation set dest failed")
|
3044 |
+
return response
|
3045 |
+
|
3046 |
+
def set_source1(self, src, flags):
|
3047 |
+
response = fluid_mod_set_source1(self.mod, src, flags)
|
3048 |
+
if response == FLUID_FAILED:
|
3049 |
+
raise Exception("Modulation set source 1 failed")
|
3050 |
+
return response
|
3051 |
+
|
3052 |
+
def set_source2(self, src, flags):
|
3053 |
+
response = fluid_mod_set_source2(self.mod, src, flags)
|
3054 |
+
if response == FLUID_FAILED:
|
3055 |
+
raise Exception("Modulation set source 2 failed")
|
3056 |
+
return response
|
3057 |
+
|
3058 |
+
def set_transform(self, type):
|
3059 |
+
response = fluid_mod_set_transform(self.mod, type)
|
3060 |
+
if response == FLUID_FAILED:
|
3061 |
+
raise Exception("Modulation set transform failed")
|
3062 |
+
return response
|
3063 |
+
|
3064 |
+
def sizeof(self):
|
3065 |
+
response = fluid_mod_sizeof()
|
3066 |
+
if response == FLUID_FAILED:
|
3067 |
+
raise Exception("Modulation sizeof failed")
|
3068 |
+
return response
|
3069 |
|
3070 |
+
def test_identity(self, mod2):
|
3071 |
+
response = fluid_mod_sizeof(self.mod, mod2)
|
3072 |
+
if response == FLUID_FAILED:
|
3073 |
+
raise Exception("Modulation identity check failed")
|
3074 |
+
return response
|
3075 |
|
3076 |
class Sequencer:
|
3077 |
def __init__(self, time_scale=1000, use_system_timer=True):
|
|
|
3088 |
def register_fluidsynth(self, synth):
|
3089 |
response = fluid_sequencer_register_fluidsynth(self.sequencer, synth.synth)
|
3090 |
if response == FLUID_FAILED:
|
3091 |
+
raise Exception("Registering fluid synth failed")
|
3092 |
return response
|
3093 |
|
3094 |
def register_client(self, name, callback, data=None):
|
3095 |
c_callback = CFUNCTYPE(None, c_uint, c_void_p, c_void_p, c_void_p)(callback)
|
3096 |
response = fluid_sequencer_register_client(self.sequencer, name.encode(), c_callback, data)
|
3097 |
if response == FLUID_FAILED:
|
3098 |
+
raise Exception("Registering client failed")
|
3099 |
|
3100 |
# store in a list to prevent garbage collection
|
3101 |
self.client_callbacks.append(c_callback)
|
|
|
3135 |
def _schedule_event(self, evt, time, absolute=True):
|
3136 |
response = fluid_sequencer_send_at(self.sequencer, evt, time, absolute)
|
3137 |
if response == FLUID_FAILED:
|
3138 |
+
raise Exception("Scheduling event failed")
|
3139 |
|
3140 |
def get_tick(self):
|
3141 |
return fluid_sequencer_get_tick(self.sequencer)
|
|
|
3154 |
|
3155 |
"""
|
3156 |
import numpy
|
3157 |
+
return (data.astype(numpy.int16)).tobytes()
|
3158 |
|
3159 |
#===============================================================================
|
3160 |
|
3161 |
import numpy as np
|
3162 |
import wave
|
3163 |
|
3164 |
+
#===============================================================================
|
3165 |
+
|
3166 |
+
def normalize_audio(audio: np.ndarray,
|
3167 |
+
method: str = 'peak',
|
3168 |
+
target_level_db: float = -1.0,
|
3169 |
+
per_channel: bool = False,
|
3170 |
+
eps: float = 1e-9
|
3171 |
+
) -> np.ndarray:
|
3172 |
+
|
3173 |
+
"""
|
3174 |
+
Normalize audio to a target dBFS level.
|
3175 |
+
|
3176 |
+
Parameters
|
3177 |
+
----------
|
3178 |
+
audio : np.ndarray
|
3179 |
+
Float-valued array in range [-1, 1] with shape (channels, samples)
|
3180 |
+
or (samples,) for mono.
|
3181 |
+
method : {'peak', 'rms'}
|
3182 |
+
- 'peak': scale so that max(|audio|) = target_level_lin
|
3183 |
+
- 'rms' : scale so that RMS(audio) = target_level_lin
|
3184 |
+
target_level_db : float
|
3185 |
+
Desired output level, in dBFS (0 dBFS = max digital full scale).
|
3186 |
+
e.g. -1.0 dBFS means ~0.8913 linear gain.
|
3187 |
+
per_channel : bool
|
3188 |
+
If True, normalize each channel independently. Otherwise, use a
|
3189 |
+
global measure across all channels.
|
3190 |
+
eps : float
|
3191 |
+
Small constant to avoid division by zero.
|
3192 |
+
|
3193 |
+
Returns
|
3194 |
+
-------
|
3195 |
+
normalized : np.ndarray
|
3196 |
+
Audio array of same shape, scaled so that levels meet the target.
|
3197 |
+
"""
|
3198 |
+
|
3199 |
+
# Convert target dB to linear gain
|
3200 |
+
target_lin = 10 ** (target_level_db / 20.0)
|
3201 |
+
|
3202 |
+
# Ensure audio is float
|
3203 |
+
audio = audio.astype(np.float32)
|
3204 |
+
|
3205 |
+
# if mono, make it (1, N)
|
3206 |
+
if audio.ndim == 1:
|
3207 |
+
audio = audio[np.newaxis, :]
|
3208 |
+
|
3209 |
+
# Choose measurement axis
|
3210 |
+
axis = 1 if per_channel else None
|
3211 |
+
|
3212 |
+
if method == 'peak':
|
3213 |
+
# Compute peak per channel or global
|
3214 |
+
peak = np.max(np.abs(audio), axis=axis, keepdims=True)
|
3215 |
+
peak = np.maximum(peak, eps)
|
3216 |
+
scales = target_lin / peak
|
3217 |
+
|
3218 |
+
elif method == 'rms':
|
3219 |
+
# Compute RMS per channel or global
|
3220 |
+
rms = np.sqrt(np.mean(audio ** 2, axis=axis, keepdims=True))
|
3221 |
+
rms = np.maximum(rms, eps)
|
3222 |
+
scales = target_lin / rms
|
3223 |
+
|
3224 |
+
else:
|
3225 |
+
raise ValueError(f"Unsupported method '{method}'; choose 'peak' or 'rms'.")
|
3226 |
+
|
3227 |
+
# Broadcast scales back to audio shape
|
3228 |
+
normalized = audio * scales
|
3229 |
+
|
3230 |
+
# Clip just in case of rounding
|
3231 |
+
return np.clip(normalized, -1.0, 1.0)
|
3232 |
+
|
3233 |
+
#===============================================================================
|
3234 |
+
|
3235 |
def midi_opus_to_colab_audio(midi_opus,
|
3236 |
soundfont_path='/usr/share/sounds/sf2/FluidR3_GM.sf2',
|
3237 |
sample_rate=16000, # 44100
|
3238 |
+
volume_level_db=-1,
|
3239 |
trim_silence=True,
|
3240 |
silence_threshold=0.1,
|
3241 |
output_for_gradio=False,
|
3242 |
write_audio_to_WAV=''
|
3243 |
):
|
3244 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3245 |
if midi_opus[1]:
|
3246 |
|
3247 |
+
ticks_per_beat, *tracks = midi_opus
|
3248 |
+
if not tracks:
|
3249 |
+
return None
|
3250 |
+
|
3251 |
+
# Flatten & convert delta-times to absolute-time
|
3252 |
+
events = []
|
3253 |
+
for track in tracks:
|
3254 |
+
abs_t = 0
|
3255 |
+
for name, dt, *data in track:
|
3256 |
+
abs_t += dt
|
3257 |
+
events.append([name, abs_t, *data])
|
3258 |
+
events.sort(key=lambda e: e[1])
|
3259 |
+
|
3260 |
+
# Setup FluidSynth
|
3261 |
+
fl = Synth(samplerate=float(sample_rate))
|
3262 |
+
sfid = fl.sfload(soundfont_path)
|
3263 |
+
for chan in range(16):
|
3264 |
+
# channel 9 = percussion GM bank 128
|
3265 |
+
fl.program_select(chan, sfid, 128 if chan == 9 else 0, 0)
|
3266 |
+
|
3267 |
+
# Playback vars
|
3268 |
+
tempo = int((60 / 120) * 1e6) # default 120bpm
|
3269 |
+
last_t = 0
|
3270 |
+
ss = np.empty((0, 2), dtype=np.int16)
|
3271 |
+
|
3272 |
+
for name, cur_t, *data in events:
|
3273 |
+
# compute how many samples have passed since the last event
|
3274 |
+
delta_ticks = cur_t - last_t
|
3275 |
+
last_t = cur_t
|
3276 |
+
dt_seconds = (delta_ticks / ticks_per_beat) * (tempo / 1e6)
|
3277 |
+
sample_len = int(dt_seconds * sample_rate)
|
3278 |
+
if sample_len > 0:
|
3279 |
+
buf = fl.get_samples(sample_len).reshape(-1, 2)
|
3280 |
+
ss = np.concatenate([ss, buf], axis=0)
|
3281 |
+
|
3282 |
+
# Dispatch every known event
|
3283 |
+
if name == "note_on" and data[2] > 0:
|
3284 |
+
chan, note, vel = data
|
3285 |
+
fl.noteon(chan, note, vel)
|
3286 |
+
|
3287 |
+
elif name == "note_off" or (name == "note_on" and data[2] == 0):
|
3288 |
+
chan, note = data[:2]
|
3289 |
+
fl.noteoff(chan, note)
|
3290 |
+
|
3291 |
+
elif name == "patch_change":
|
3292 |
+
chan, patch = data[:2]
|
3293 |
+
bank = 128 if chan == 9 else 0
|
3294 |
+
fl.program_select(chan, sfid, bank, patch)
|
3295 |
+
|
3296 |
+
elif name == "control_change":
|
3297 |
+
chan, ctrl, val = data[:3]
|
3298 |
+
fl.cc(chan, ctrl, val)
|
3299 |
+
|
3300 |
+
elif name == "key_after_touch":
|
3301 |
+
chan, note, vel = data
|
3302 |
+
fl.key_pressure(chan, note, vel)
|
3303 |
+
|
3304 |
+
elif name == "channel_after_touch":
|
3305 |
+
chan, vel = data
|
3306 |
+
fl.channel_pressure(chan, vel)
|
3307 |
+
|
3308 |
+
elif name == "pitch_wheel_change":
|
3309 |
+
chan, wheel = data
|
3310 |
+
fl.pitch_bend(chan, wheel)
|
3311 |
+
|
3312 |
+
elif name == "song_position":
|
3313 |
+
# song_pos = data[0]; # often not needed for playback
|
3314 |
+
pass
|
3315 |
+
|
3316 |
+
elif name == "song_select":
|
3317 |
+
# song_number = data[0]
|
3318 |
+
pass
|
3319 |
+
|
3320 |
+
elif name == "tune_request":
|
3321 |
+
# typically resets tuning; FS handles internally
|
3322 |
+
pass
|
3323 |
+
|
3324 |
+
elif name in ("sysex_f0", "sysex_f7"):
|
3325 |
+
raw_bytes = data[0]
|
3326 |
+
fl.sysex(raw_bytes)
|
3327 |
+
|
3328 |
+
# Meta events & others—no direct audio effect, so we skip or log
|
3329 |
+
elif name in (
|
3330 |
+
"set_tempo", # handled below
|
3331 |
+
"end_track",
|
3332 |
+
"text_event", "text_event_08", "text_event_09", "text_event_0a",
|
3333 |
+
"text_event_0b", "text_event_0c", "text_event_0d", "text_event_0e", "text_event_0f",
|
3334 |
+
"copyright_text_event", "track_name", "instrument_name",
|
3335 |
+
"lyric", "marker", "cue_point",
|
3336 |
+
"smpte_offset", "time_signature", "key_signature",
|
3337 |
+
"sequencer_specific", "raw_meta_event"
|
3338 |
+
):
|
3339 |
+
if name == "set_tempo":
|
3340 |
+
tempo = data[0]
|
3341 |
+
# else: skip all other meta & text; you could hook in logging here
|
3342 |
+
continue
|
3343 |
+
|
3344 |
+
else:
|
3345 |
+
# unknown event type
|
3346 |
+
continue
|
3347 |
+
|
3348 |
+
# Cleanup synth
|
3349 |
+
fl.delete()
|
3350 |
+
|
3351 |
+
if ss.size:
|
3352 |
+
maxv = np.abs(ss).max()
|
3353 |
+
if maxv:
|
3354 |
+
ss = (ss / maxv) * np.iinfo(np.int16).max
|
3355 |
+
ss = ss.astype(np.int16)
|
3356 |
+
|
3357 |
+
# Optional trimming of trailing silence
|
3358 |
+
if trim_silence and ss.size:
|
3359 |
+
thresh = np.std(np.abs(ss)) * silence_threshold
|
3360 |
+
idx = np.where(np.abs(ss) > thresh)[0]
|
3361 |
+
if idx.size:
|
3362 |
+
ss = ss[: idx[-1] + 1]
|
3363 |
+
|
3364 |
+
# For Gradio you might want raw int16 PCM
|
3365 |
+
if output_for_gradio:
|
3366 |
+
return ss
|
3367 |
+
|
3368 |
+
# Swap to (channels, samples) and normalize for playback
|
3369 |
+
ss = ss.T
|
3370 |
+
raw_audio = normalize_audio(ss, target_level_db=volume_level_db)
|
3371 |
+
|
3372 |
+
# Optionally write WAV to disk
|
3373 |
+
if write_audio_to_WAV:
|
3374 |
+
wav_name = midi_file.rsplit('.', 1)[0] + '.wav'
|
3375 |
+
pcm = np.int16(raw_audio.T / np.max(np.abs(raw_audio)) * 32767)
|
3376 |
+
with wave.open(wav_name, 'wb') as wf:
|
3377 |
+
wf.setframerate(sample_rate)
|
3378 |
+
wf.setsampwidth(2)
|
3379 |
+
wf.setnchannels(pcm.shape[1])
|
3380 |
+
wf.writeframes(pcm.tobytes())
|
3381 |
+
|
3382 |
+
return raw_audio
|
3383 |
|
3384 |
else:
|
3385 |
return None
|
3386 |
|
3387 |
+
#===============================================================================
|
3388 |
+
|
3389 |
+
def midi_to_colab_audio(midi_file,
|
3390 |
+
soundfont_path='/usr/share/sounds/sf2/FluidR3_GM.sf2',
|
3391 |
+
sample_rate=16000,
|
3392 |
+
volume_level_db=-1,
|
3393 |
trim_silence=True,
|
3394 |
silence_threshold=0.1,
|
3395 |
output_for_gradio=False,
|
3396 |
write_audio_to_WAV=False
|
3397 |
+
):
|
3398 |
+
"""
|
|
|
|
|
3399 |
Returns raw audio to pass to IPython.disaply.Audio func
|
3400 |
|
3401 |
Example usage:
|
|
|
3403 |
from IPython.display import Audio
|
3404 |
|
3405 |
display(Audio(raw_audio, rate=16000, normalize=False))
|
3406 |
+
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3407 |
|
3408 |
+
# Read and decode MIDI → opus event list
|
3409 |
+
ticks_per_beat, *tracks = midi2opus(open(midi_file, 'rb').read())
|
3410 |
+
if not tracks:
|
3411 |
+
return None
|
3412 |
|
3413 |
+
# Flatten & convert delta-times to absolute-time
|
3414 |
+
events = []
|
3415 |
+
for track in tracks:
|
3416 |
+
abs_t = 0
|
3417 |
+
for name, dt, *data in track:
|
3418 |
+
abs_t += dt
|
3419 |
+
events.append([name, abs_t, *data])
|
3420 |
+
events.sort(key=lambda e: e[1])
|
3421 |
+
|
3422 |
+
# Setup FluidSynth
|
3423 |
+
fl = Synth(samplerate=float(sample_rate))
|
3424 |
+
sfid = fl.sfload(soundfont_path)
|
3425 |
+
for chan in range(16):
|
3426 |
+
# channel 9 = percussion GM bank 128
|
3427 |
+
fl.program_select(chan, sfid, 128 if chan == 9 else 0, 0)
|
3428 |
+
|
3429 |
+
# Playback vars
|
3430 |
+
tempo = int((60 / 120) * 1e6) # default 120bpm
|
3431 |
+
last_t = 0
|
3432 |
+
ss = np.empty((0, 2), dtype=np.int16)
|
3433 |
+
|
3434 |
+
for name, cur_t, *data in events:
|
3435 |
+
# compute how many samples have passed since the last event
|
3436 |
+
delta_ticks = cur_t - last_t
|
3437 |
+
last_t = cur_t
|
3438 |
+
dt_seconds = (delta_ticks / ticks_per_beat) * (tempo / 1e6)
|
3439 |
+
sample_len = int(dt_seconds * sample_rate)
|
3440 |
+
if sample_len > 0:
|
3441 |
+
buf = fl.get_samples(sample_len).reshape(-1, 2)
|
3442 |
+
ss = np.concatenate([ss, buf], axis=0)
|
3443 |
+
|
3444 |
+
# Dispatch every known event
|
3445 |
+
if name == "note_on" and data[2] > 0:
|
3446 |
+
chan, note, vel = data
|
3447 |
+
fl.noteon(chan, note, vel)
|
3448 |
+
|
3449 |
+
elif name == "note_off" or (name == "note_on" and data[2] == 0):
|
3450 |
+
chan, note = data[:2]
|
3451 |
+
fl.noteoff(chan, note)
|
3452 |
+
|
3453 |
+
elif name == "patch_change":
|
3454 |
+
chan, patch = data[:2]
|
3455 |
+
bank = 128 if chan == 9 else 0
|
3456 |
+
fl.program_select(chan, sfid, bank, patch)
|
3457 |
+
|
3458 |
+
elif name == "control_change":
|
3459 |
+
chan, ctrl, val = data[:3]
|
3460 |
+
fl.cc(chan, ctrl, val)
|
3461 |
+
|
3462 |
+
elif name == "key_after_touch":
|
3463 |
+
chan, note, vel = data
|
3464 |
+
fl.key_pressure(chan, note, vel)
|
3465 |
+
|
3466 |
+
elif name == "channel_after_touch":
|
3467 |
+
chan, vel = data
|
3468 |
+
fl.channel_pressure(chan, vel)
|
3469 |
+
|
3470 |
+
elif name == "pitch_wheel_change":
|
3471 |
+
chan, wheel = data
|
3472 |
+
fl.pitch_bend(chan, wheel)
|
3473 |
+
|
3474 |
+
elif name == "song_position":
|
3475 |
+
# song_pos = data[0]; # often not needed for playback
|
3476 |
+
pass
|
3477 |
+
|
3478 |
+
elif name == "song_select":
|
3479 |
+
# song_number = data[0]
|
3480 |
+
pass
|
3481 |
+
|
3482 |
+
elif name == "tune_request":
|
3483 |
+
# typically resets tuning; FS handles internally
|
3484 |
+
pass
|
3485 |
+
|
3486 |
+
elif name in ("sysex_f0", "sysex_f7"):
|
3487 |
+
raw_bytes = data[0]
|
3488 |
+
fl.sysex(raw_bytes)
|
3489 |
+
|
3490 |
+
# Meta events & others—no direct audio effect, so we skip or log
|
3491 |
+
elif name in (
|
3492 |
+
"set_tempo", # handled below
|
3493 |
+
"end_track",
|
3494 |
+
"text_event", "text_event_08", "text_event_09", "text_event_0a",
|
3495 |
+
"text_event_0b", "text_event_0c", "text_event_0d", "text_event_0e", "text_event_0f",
|
3496 |
+
"copyright_text_event", "track_name", "instrument_name",
|
3497 |
+
"lyric", "marker", "cue_point",
|
3498 |
+
"smpte_offset", "time_signature", "key_signature",
|
3499 |
+
"sequencer_specific", "raw_meta_event"
|
3500 |
+
):
|
3501 |
+
if name == "set_tempo":
|
3502 |
+
tempo = data[0]
|
3503 |
+
# else: skip all other meta & text; you could hook in logging here
|
3504 |
+
continue
|
3505 |
|
3506 |
+
else:
|
3507 |
+
# unknown event type
|
3508 |
+
continue
|
3509 |
|
3510 |
+
# Cleanup synth
|
3511 |
+
fl.delete()
|
3512 |
|
3513 |
+
if ss.size:
|
3514 |
+
maxv = np.abs(ss).max()
|
3515 |
+
if maxv:
|
3516 |
+
ss = (ss / maxv) * np.iinfo(np.int16).max
|
3517 |
+
ss = ss.astype(np.int16)
|
3518 |
|
3519 |
+
# Optional trimming of trailing silence
|
3520 |
+
if trim_silence and ss.size:
|
3521 |
+
thresh = np.std(np.abs(ss)) * silence_threshold
|
3522 |
+
idx = np.where(np.abs(ss) > thresh)[0]
|
3523 |
+
if idx.size:
|
3524 |
+
ss = ss[: idx[-1] + 1]
|
3525 |
|
3526 |
+
# For Gradio you might want raw int16 PCM
|
3527 |
+
if output_for_gradio:
|
3528 |
+
return ss
|
3529 |
|
3530 |
+
# Swap to (channels, samples) and normalize for playback
|
3531 |
+
ss = ss.T
|
3532 |
+
raw_audio = normalize_audio(ss, target_level_db=volume_level_db)
|
3533 |
|
3534 |
+
# Optionally write WAV to disk
|
3535 |
+
if write_audio_to_WAV:
|
3536 |
+
wav_name = midi_file.rsplit('.', 1)[0] + '.wav'
|
3537 |
+
pcm = np.int16(raw_audio.T / np.max(np.abs(raw_audio)) * 32767)
|
3538 |
+
with wave.open(wav_name, 'wb') as wf:
|
3539 |
wf.setframerate(sample_rate)
|
3540 |
wf.setsampwidth(2)
|
3541 |
+
wf.setnchannels(pcm.shape[1])
|
3542 |
+
wf.writeframes(pcm.tobytes())
|
3543 |
+
|
3544 |
+
return raw_audio
|
3545 |
|
|
|
|
|
|
|
|
|
|
|
3546 |
#===================================================================================================================
|