Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 47 additions & 3 deletions lib/audioinstruments/soundfont.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@
the host's. A program change puts every macro back to the patch's defaults,
Pan at centre.

The pitch wheel bends every note of the instrument by up to `bend_range`
semitones either way, two until a host sets it (General MIDI's Pitch Bend
Sensitivity, which a host reads from RPN 0). `tuning` shifts the whole
instrument by a number of semitones, fractions included, for a host's
RPN 1 and 2 (Channel Fine and Coarse Tuning); it is 0 until set. Both apply
to the notes already sounding, and `reset()` puts them back, with the
wheel centred.

Thirty-two engine voices, one per region a note resolves to (a stereo pair
is two). A new note with every voice busy steals the oldest releasing voice,
then the oldest. A note in an exclusive class stops the sounding notes of
Expand Down Expand Up @@ -947,6 +955,9 @@ def __init__(self, bank, voices, default_bank=0):
self.release_scale = 1.0
self.tune = 1.0
self.bend = 1.0
self.bend_amount = 0.0 # the wheel, -1 to 1
self.bend_range = BEND_RANGE
self.tuning = 1.0 # the host's channel tuning, as a ratio
self.pan = 0.0
self.volume_cc = {} # channel -> Control Change 7
self.expression = {} # channel -> Control Change 11
Expand Down Expand Up @@ -1063,7 +1074,7 @@ def note_on(self, key, pitch, velocity, detune):
i = self._allocate()
voices[i].play(
pcm, rate=region.sample_rate,
pitch=ratio * self.tune * self.bend,
pitch=ratio * self.tune * self.bend * self.tuning,
loop_start=loop_start, loop_end=loop_end, loop_mode=mode,
level=level * controllers * self.volume * pool.gain,
pan=_panned(region.pan, self.pan),
Expand Down Expand Up @@ -1169,7 +1180,7 @@ def revibrato(self, channel):

def repitch(self):
voices = self.pool.voice
factor = self.tune * self.bend
factor = self.tune * self.bend * self.tuning
for i, slot in enumerate(self.slots):
if slot is not None and slot[7] is self:
voices[i].pitch = slot[5] * factor
Expand Down Expand Up @@ -1209,7 +1220,8 @@ def handle_event(self, event_type, channel, note_id, data0, value0,
self.pan = _bipolar(value0)
self.repan()
elif event_type == EVENT_PITCH_BEND:
self.bend = 2.0 ** (value1 * BEND_RANGE / 12.0)
self.bend_amount = value1
self.bend = 2.0 ** (value1 * self.bend_range / 12.0)
self.repitch()
elif event_type == EVENT_CHANNEL_PRESSURE:
self.pressure[channel] = value0 * PRESSURE_TO_VIBRATO
Expand Down Expand Up @@ -1243,6 +1255,7 @@ class SoundFontInstrument(Instrument):
def __init__(self, player, patches, programs, macro_labels, note_map=None,
transport=None, bank_select=True):
self._player = player
self._tuning = 0.0
self._programs = programs
self._bank_select = bank_select
super().__init__(None, player.handle_event, patches, macro_labels,
Expand All @@ -1263,6 +1276,32 @@ def preset(self):
def preset_name(self):
return self._player.bank.presets.get(self.preset)

@property
def bend_range(self):
"""Semitones the pitch wheel bends at full deflection, either way:
`BEND_RANGE` (2) until set. Setting it moves the notes sounding
with the wheel where it is."""
return self._player.bend_range

@bend_range.setter
def bend_range(self, semitones):
player = self._player
player.bend_range = float(semitones)
player.bend = 2.0 ** (player.bend_amount * player.bend_range / 12.0)
player.repitch()

@property
def tuning(self):
"""The whole instrument's shift in semitones, fractions included:
0 until set."""
return self._tuning

@tuning.setter
def tuning(self, semitones):
self._tuning = float(semitones)
self._player.tuning = 2.0 ** (self._tuning / 12.0)
self._player.repitch()

def control_change(self, controller, value, channel=0, sample_position=0):
"""Control Change 10 (Pan) moves the Pan macro, so the macro value
and the sound agree; the rest reach the player as they are.
Expand Down Expand Up @@ -1291,6 +1330,11 @@ def reset(self):
for state in (self._player.volume_cc, self._player.expression,
self._player.wheel, self._player.pressure):
state.clear()
self._player.bend_amount = 0.0
self._player.bend = 1.0
self._player.bend_range = BEND_RANGE
self._tuning = 0.0
self._player.tuning = 1.0
super().reset()

@property
Expand Down
34 changes: 34 additions & 0 deletions tests/test_cpython_soundfont.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,40 @@ def test_macros_reach_held_voices(self):
self.inst.set_macro(2, 0)
self.assertAlmostEqual(player.release_scale, 0.25, places=2)

def test_bend_range_and_tuning_move_held_voices(self):
player = self.inst._player
self.assertEqual(self.inst.bend_range, soundfont.BEND_RANGE)
self.assertEqual(self.inst.tuning, 0.0)
self.inst.note_on(60, 100)
slot = player.held[(0, 60)][0]
voice = self.inst.output.voice[slot]
ratio = player.slots[slot][5]
# Half the wheel down, then the range widened under it: the note
# follows without another bend message.
self.inst.pitch_bend(4096)
self.assertAlmostEqual(voice.pitch, ratio * 2 ** (-1 / 12.0), places=4)
self.inst.bend_range = 12
self.assertAlmostEqual(voice.pitch, ratio * 2 ** (-6 / 12.0), places=4)
self.inst.pitch_bend(0)
self.assertAlmostEqual(voice.pitch, ratio * 2 ** (-12 / 12.0), places=4)
self.inst.pitch_bend(4096)
self.inst.tuning = 2.5
self.assertAlmostEqual(voice.pitch, ratio * 2 ** (-3.5 / 12.0), places=4)
# A new note starts where the held one is.
self.inst.note_on(64, 100)
slot2 = player.held[(0, 64)][0]
self.assertAlmostEqual(self.inst.output.voice[slot2].pitch,
player.slots[slot2][5] * 2 ** (-3.5 / 12.0),
places=4)
# reset() puts the wheel, its range and the tuning back.
self.inst.reset()
self.assertEqual(self.inst.bend_range, soundfont.BEND_RANGE)
self.assertEqual(self.inst.tuning, 0.0)
self.inst.note_on(60, 100)
slot = player.held[(0, 60)][0]
self.assertAlmostEqual(self.inst.output.voice[slot].pitch,
player.slots[slot][5], places=5)

# -- pan ------------------------------------------------------------------

PAN = soundfont.PAN_MACRO
Expand Down
Loading