asigalov61 commited on
Commit
36ec2c2
·
verified ·
1 Parent(s): b62d385

Upload 2 files

Browse files
Files changed (2) hide show
  1. TMIDIX.py +129 -43
  2. midi_to_colab_audio.py +679 -223
TMIDIX.py CHANGED
@@ -51,7 +51,7 @@ r'''############################################################################
51
 
52
  ###################################################################################
53
 
54
- __version__ = "25.7.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, max_velocity):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3728
 
3729
- min_velocity = min([c[5] for c in score])
3730
- max_velocity_all_channels = max([c[5] for c in score])
3731
- min_velocity_ratio = min_velocity / max_velocity_all_channels
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- factor = max_velocity / max_channel_velocity
3738
- for i in range(len(score)):
3739
- score[i][5] = int(score[i][5] * factor)
 
 
 
 
 
 
 
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(enhanced_score_notes,
5033
- default_patch=0,
5034
- drums_patch=9,
5035
- verbose=False
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
- if e[0] == 'note':
5048
- if e[3] != 9:
5049
- if patches[e[3]] == -1:
5050
- patches[e[3]] = e[6]
5051
- else:
5052
- if patches[e[3]] != e[6]:
5053
- if e[6] in patches:
5054
- e[3] = patches.index(e[6])
5055
- else:
5056
- if -1 in patches:
5057
- patches[patches.index(-1)] = e[6]
5058
- else:
5059
- overflow_idx = idx
5060
- break
 
 
 
 
 
 
 
5061
 
5062
- enhanced_score_notes_with_patch_changes.append(e)
5063
 
5064
  #===========================================================================
5065
 
5066
  overflow_patches = []
 
 
 
 
 
5067
 
5068
  if overflow_idx != -1:
5069
- for idx, e in enumerate(enhanced_score_notes[overflow_idx:]):
5070
- if e[0] == 'note':
5071
- if e[3] != 9:
5072
- if e[6] not in patches:
5073
- if e[6] not in overflow_patches:
5074
- overflow_patches.append(e[6])
5075
- enhanced_score_notes_with_patch_changes.append(['patch_change', e[1], e[3], e[6]])
5076
- else:
5077
- e[3] = patches.index(e[6])
 
 
 
 
 
 
 
 
 
 
 
 
5078
 
5079
- enhanced_score_notes_with_patch_changes.append(e)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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('Composition patches')
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 1.0
9
  #
10
- # Includes full source code of MIDI, pyfluidsynth, and midi_synthesizer Python modules
11
  #
12
- # Original source code for all modules was retrieved on 10/23/2023
13
  #
14
  # Project Los Angeles
15
- # Tegridy Code 2023
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
- # A short circuited or expression to find the FluidSynth library
1798
- # (mostly needed for Windows distributions of libfluidsynth supplied with QSynth)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- lib = find_library('fluidsynth') or \
1806
- find_library('libfluidsynth') or \
1807
- find_library('libfluidsynth-3') or \
1808
- find_library('libfluidsynth-2') or \
1809
- find_library('libfluidsynth-1')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.1'
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.%s.device' % driver)
2455
  midi_driver = midi_driver or self.get_setting('midi.driver')
2456
 
2457
  self.setting('audio.driver', driver)
2458
- self.setting('audio.%s.device' % driver, device)
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 == None: ## Use fluidsynth to create a MIDI event handler
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
- (sfontid, banknum, presetnum, presetname) = self.channel_info(chan)
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
- set=0
2574
  if roomsize>=0:
2575
- set+=0b0001
2576
  if damping>=0:
2577
- set+=0b0010
2578
  if width>=0:
2579
- set+=0b0100
2580
  if level>=0:
2581
- set+=0b1000
2582
- return fluid_synth_set_reverb_full(self.synth, set, roomsize, damping, width, level)
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(leve=level)
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, depth):
2642
  if fluid_synth_set_chorus_depth is not None:
2643
- return fluid_synth_set_chorus_depth(self.synth, depth)
2644
  else:
2645
- return self.set_chorus(depth=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 +8192 (transposing by 4 semitones).
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, pitch):
2751
- return fluid_synth_tuning_dump(self.synth, bank, prog, name.encode(), length(name), pitch)
 
 
 
 
 
 
 
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 == None: return FLUID_FAILED
2771
- if self.custom_router_callback != None:
 
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: return status
 
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: return status
 
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 Error("Registering fluid synth failed")
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 Error("Registering client failed")
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 Error("Scheduling event failed")
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)).tostring()
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
- volume_scale=10,
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
- ticks_per_beat = midi_opus[0]
2898
- event_list = []
2899
- for track_idx, track in enumerate(midi_opus[1:]):
2900
- abs_t = 0
2901
- for event in track:
2902
- abs_t += event[1]
2903
- event_new = [*event]
2904
- event_new[1] = abs_t
2905
- event_list.append(event_new)
2906
- event_list = sorted(event_list, key=lambda e: e[1])
2907
-
2908
- tempo = int((60 / 120) * 10 ** 6) # default 120 bpm
2909
- ss = np.empty((0, 2), dtype=np.int16)
2910
- fl = Synth(samplerate=float(sample_rate))
2911
- sfid = fl.sfload(soundfont_path)
2912
- last_t = 0
2913
- for c in range(16):
2914
- fl.program_select(c, sfid, 128 if c == 9 else 0, 0)
2915
- for event in event_list:
2916
- name = event[0]
2917
- sample_len = int(((event[1] / ticks_per_beat) * tempo / (10 ** 6)) * sample_rate)
2918
- sample_len -= int(((last_t / ticks_per_beat) * tempo / (10 ** 6)) * sample_rate)
2919
- last_t = event[1]
2920
- if sample_len > 0:
2921
- sample = fl.get_samples(sample_len).reshape(sample_len, 2)
2922
- ss = np.concatenate([ss, sample])
2923
- if name == "set_tempo":
2924
- tempo = event[2]
2925
- elif name == "patch_change":
2926
- c, p = event[2:4]
2927
- fl.program_select(c, sfid, 128 if c == 9 else 0, p)
2928
- elif name == "control_change":
2929
- c, cc, v = event[2:5]
2930
- fl.cc(c, cc, v)
2931
- elif name == "note_on" and event[3] > 0:
2932
- c, p, v = event[2:5]
2933
- fl.noteon(c, p, v)
2934
- elif name == "note_off" or (name == "note_on" and event[3] == 0):
2935
- c, p = event[2:4]
2936
- fl.noteoff(c, p)
2937
-
2938
- fl.delete()
2939
- if ss.shape[0] > 0:
2940
- max_val = np.abs(ss).max()
2941
- if max_val != 0:
2942
- ss = (ss / max_val) * np.iinfo(np.int16).max
2943
- ss = ss.astype(np.int16)
2944
-
2945
- if trim_silence:
2946
- threshold = np.std(np.abs(ss)) * silence_threshold
2947
- exceeded_thresh = np.abs(ss) > threshold
2948
- if np.any(exceeded_thresh):
2949
- last_idx = np.where(exceeded_thresh)[0][-1]
2950
- ss = ss[:last_idx+1]
2951
-
2952
- if output_for_gradio:
2953
- return ss
2954
-
2955
- ss = ss.swapaxes(1, 0)
2956
-
2957
- raw_audio = normalize_volume(ss, volume_scale)
2958
-
2959
- if write_audio_to_WAV != '':
2960
-
2961
- r_audio = raw_audio.T
2962
-
2963
- r_audio = np.int16(r_audio / np.max(np.abs(r_audio)) * 32767)
2964
-
2965
- with wave.open(write_audio_to_WAV, 'w') as wf:
2966
- wf.setframerate(sample_rate)
2967
- wf.setsampwidth(2)
2968
- wf.setnchannels(r_audio.shape[1])
2969
- wf.writeframes(r_audio)
2970
-
2971
- return raw_audio
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2972
 
2973
  else:
2974
  return None
2975
 
2976
- def midi_to_colab_audio(midi_file,
2977
- soundfont_path='/usr/share/sounds/sf2/FluidR3_GM.sf2',
2978
- sample_rate=16000, # 44100
2979
- volume_scale=10,
 
 
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
- if midi_opus[1]:
 
 
 
3008
 
3009
- ticks_per_beat = midi_opus[0]
3010
- event_list = []
3011
- for track_idx, track in enumerate(midi_opus[1:]):
3012
- abs_t = 0
3013
- for event in track:
3014
- abs_t += event[1]
3015
- event_new = [*event]
3016
- event_new[1] = abs_t
3017
- event_list.append(event_new)
3018
- event_list = sorted(event_list, key=lambda e: e[1])
3019
-
3020
- tempo = int((60 / 120) * 10 ** 6) # default 120 bpm
3021
- ss = np.empty((0, 2), dtype=np.int16)
3022
- fl = Synth(samplerate=float(sample_rate))
3023
- sfid = fl.sfload(soundfont_path)
3024
- last_t = 0
3025
- for c in range(16):
3026
- fl.program_select(c, sfid, 128 if c == 9 else 0, 0)
3027
- for event in event_list:
3028
- name = event[0]
3029
- sample_len = int(((event[1] / ticks_per_beat) * tempo / (10 ** 6)) * sample_rate)
3030
- sample_len -= int(((last_t / ticks_per_beat) * tempo / (10 ** 6)) * sample_rate)
3031
- last_t = event[1]
3032
- if sample_len > 0:
3033
- sample = fl.get_samples(sample_len).reshape(sample_len, 2)
3034
- ss = np.concatenate([ss, sample])
3035
- if name == "set_tempo":
3036
- tempo = event[2]
3037
- elif name == "patch_change":
3038
- c, p = event[2:4]
3039
- fl.program_select(c, sfid, 128 if c == 9 else 0, p)
3040
- elif name == "control_change":
3041
- c, cc, v = event[2:5]
3042
- fl.cc(c, cc, v)
3043
- elif name == "note_on" and event[3] > 0:
3044
- c, p, v = event[2:5]
3045
- fl.noteon(c, p, v)
3046
- elif name == "note_off" or (name == "note_on" and event[3] == 0):
3047
- c, p = event[2:4]
3048
- fl.noteoff(c, p)
3049
-
3050
- fl.delete()
3051
- if ss.shape[0] > 0:
3052
- max_val = np.abs(ss).max()
3053
- if max_val != 0:
3054
- ss = (ss / max_val) * np.iinfo(np.int16).max
3055
- ss = ss.astype(np.int16)
3056
-
3057
- if trim_silence:
3058
- threshold = np.std(np.abs(ss)) * silence_threshold
3059
- exceeded_thresh = np.abs(ss) > threshold
3060
- if np.any(exceeded_thresh):
3061
- last_idx = np.where(exceeded_thresh)[0][-1]
3062
- ss = ss[:last_idx+1]
3063
-
3064
- if output_for_gradio:
3065
- return ss
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3066
 
3067
- ss = ss.swapaxes(1, 0)
 
 
3068
 
3069
- raw_audio = normalize_volume(ss, volume_scale)
 
3070
 
3071
- if write_audio_to_WAV:
 
 
 
 
3072
 
3073
- filename = midi_file.split('.')[-2] + '.wav'
 
 
 
 
 
3074
 
3075
- r_audio = raw_audio.T
 
 
3076
 
3077
- r_audio = np.int16(r_audio / np.max(np.abs(r_audio)) * 32767)
 
 
3078
 
3079
- with wave.open(filename, 'w') as wf:
 
 
 
 
3080
  wf.setframerate(sample_rate)
3081
  wf.setsampwidth(2)
3082
- wf.setnchannels(r_audio.shape[1])
3083
- wf.writeframes(r_audio)
 
 
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
  #===================================================================================================================