"""
This module defines the :class:`~scamp_extensions.playback.multi_staff_instrument.MultiStaffInstrument` class,
a convenience class that combines multiple :class:`ScampInstrument` objects so that they can create a single part in
the score. A typical use case would be to combine arco, pizzicato and harmonic presets from a soundfont into a single
string instrument part.
"""
# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ #
# This file is part of SCAMP (Suite for Computer-Assisted Music in Python) #
# Copyright © 2020 Marc Evanstein <marc@marcevanstein.com>. #
# #
# This program is free software: you can redistribute it and/or modify it under the terms of #
# the GNU General Public License as published by the Free Software Foundation, either version #
# 3 of the License, or (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; #
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. #
# See the GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License along with this program. #
# If not, see <http://www.gnu.org/licenses/>. #
# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ #
from __future__ import annotations
from clockblocks import Clock
from scamp import ScampInstrument, SpellingPolicy, Envelope
from scamp.utilities import NoteProperty
from typing import Callable, Sequence
[docs]
class MultiStaffInstrument:
"""
A convenient wrapper for bundling multiple `ScampInstrument` objects into a single meta instrument that
represents a grand staff of group of staves.
:param name: name of this instrument
:param staff_parts: list of ScampInstrument objects that represent the different staves
:param decision_protocol: either a tuple of pitch boundaries that decide which part plays
or a function that maps pitch to part index.
"""
def __init__(self, name: str, staff_parts: Sequence[ScampInstrument],
decision_protocol: Callable[[float], int] | Sequence = (60,)):
self.name = name
self.staff_parts = staff_parts
if isinstance(decision_protocol, Callable):
self.decision_function = decision_protocol
else:
assert len(decision_protocol) + 1 == len(staff_parts), \
f"Wrong number of decision boundaries for {len(staff_parts)} instruments"
def decision_function(p):
i = 0
for pitch_boundary in decision_protocol:
if p >= pitch_boundary:
i += 1
else:
break
return len(decision_protocol) - i
self.decision_function = decision_function
@staticmethod
def _pitch_value_to_float(pitch):
if isinstance(pitch, Envelope):
return pitch.start_level()
elif hasattr(pitch, '__len__'):
return Envelope.from_list(pitch).start_level()
else:
return pitch
[docs]
def play_note(self, pitch, volume, length, properties: Union[str, dict, Sequence, NoteProperty] = None,
force_staff: int = None, blocking: bool = True, clock: Clock = None) -> None:
"""
Play a note using this MultiStaffInstrument
:param pitch: see :func:`~scamp.instruments.ScampInstrument.play_note`
:param volume: see :func:`~scamp.instruments.ScampInstrument.play_note`
:param length: see :func:`~scamp.instruments.ScampInstrument.play_note`
:param force_staff: force this note to play on the given staff index
:param properties: see :func:`~scamp.instruments.ScampInstrument.play_note`
:param blocking: see :func:`~scamp.instruments.ScampInstrument.play_note`
:param clock: see :func:`~scamp.instruments.ScampInstrument.play_note`
"""
if pitch is None:
# so that we don't call the decision function on a rest
staff_index = 0
elif force_staff:
staff_index = force_staff
else:
staff_index = self.decision_function(MultiStaffInstrument._pitch_value_to_float(pitch))
self.staff_parts[staff_index].play_note(
pitch, volume, length, properties=properties, blocking=blocking, clock=clock
)
[docs]
def play_chord(self, pitches: Sequence, volume, length, properties: Union[str, dict, Sequence, NoteProperty] = None,
force_staff: int = None, blocking: bool = True, clock: Clock = None) -> None:
"""
Play a chord using this MultiPresetInstrument.
:param pitches: see :func:`~scamp.instruments.ScampInstrument.play_chord`
:param volume: see :func:`~scamp.instruments.ScampInstrument.play_chord`
:param length: see :func:`~scamp.instruments.ScampInstrument.play_chord`
:param properties: see :func:`~scamp.instruments.ScampInstrument.play_chord`
:param force_staff: force this chord to play on the given staff index
:param blocking: see :func:`~scamp.instruments.ScampInstrument.play_chord`
:param clock: see :func:`~scamp.instruments.ScampInstrument.play_chord`
"""
if pitches is None:
# so that we don't call the decision function on a rest
staff_index = 0
elif force_staff:
staff_index = force_staff
else:
average_pitch = sum(
MultiStaffInstrument._pitch_value_to_float(pitch)
for pitch in pitches
) / len(pitches)
staff_index = self.decision_function(average_pitch)
self.staff_parts[staff_index].play_chord(
pitches, volume, length, properties=properties, blocking=blocking, clock=clock
)
[docs]
def start_note(self, pitch: float, volume: float, properties: Union[str, dict, Sequence, NoteProperty] = None,
force_staff: int = None, clock: Clock = None, max_volume: float = 1) -> MultiNoteHandle:
"""
Start a note using this MultiPresetInstrument.
:param pitch: see :func:`~scamp.instruments.ScampInstrument.start_note`
:param volume: see :func:`~scamp.instruments.ScampInstrument.start_note`
:param properties: see :func:`~scamp.instruments.ScampInstrument.start_note`
:param force_staff: force this note to play on the given staff index
:param clock: see :func:`~scamp.instruments.ScampInstrument.start_note`
:param max_volume: see :func:`~scamp.instruments.ScampInstrument.start_note`
:return: a :class:`MultiNoteHandle` with which to later manipulate the note
"""
if pitch is None:
# so that we don't call the decision function on a rest
staff_index = 0
elif force_staff:
staff_index = force_staff
else:
staff_index = self.decision_function(MultiStaffInstrument._pitch_value_to_float(pitch))
return self.staff_parts[staff_index].start_note(
pitch, volume, properties,
clock=clock, max_volume=max_volume
)
[docs]
def start_chord(self, pitches: Sequence[float], volume: float,
properties: Union[str, dict, Sequence, NoteProperty] = None, force_staff: int = None,
clock: Clock = None, max_volume: float = 1) -> MultiNoteHandle:
"""
Start a note using this MultiPresetInstrument.
:param pitches: see :func:`~scamp.instruments.ScampInstrument.start_chord`
:param volume: see :func:`~scamp.instruments.ScampInstrument.start_chord`
:param properties: see :func:`~scamp.instruments.ScampInstrument.start_chord`
:param force_staff: force this chord to play on the given staff index
:param clock: see :func:`~scamp.instruments.ScampInstrument.start_chord`
:param max_volume: see :func:`~scamp.instruments.ScampInstrument.start_chord`
:return: a :class:`MultiNoteHandle` with which to later manipulate the chord
"""
if pitches is None:
# so that we don't call the decision function on a rest
staff_index = 0
elif force_staff:
staff_index = force_staff
else:
average_pitch = sum(
MultiStaffInstrument._pitch_value_to_float(pitch)
for pitch in pitches
) / len(pitches)
staff_index = self.decision_function(average_pitch)
return self.staff_parts[staff_index].start_chord(
pitches, volume, properties,
clock=clock, max_volume=max_volume
)
[docs]
def send_midi_cc(self, cc_number: int, value_from_0_to_1: float) -> None:
"""
Send a midi cc message to every :class:`~scamp.instruments.ScampInstrument:` used by this MultiPresetInstrument.
:param cc_number: MIDI cc number
:param value_from_0_to_1: value to send (scaled from 0 to 1)
"""
for instrument in self.staff_parts:
instrument.send_midi_cc(cc_number, value_from_0_to_1)
[docs]
def end_all_notes(self) -> None:
"""
Ends all notes currently playing
"""
for instrument in self.staff_parts:
instrument.end_all_notes()
[docs]
def num_notes_playing(self) -> int:
"""
Returns the number of notes currently playing.
"""
return sum(instrument.num_notes_playing() for instrument in self.staff_parts)
[docs]
def set_max_pitch_bend(self, semitones: int) -> None:
"""
Set the max pitch bend for all midi playback implementations on this instrument
"""
for instrument in self.staff_parts:
instrument.set_max_pitch_bend(semitones)
@property
def default_spelling_policy(self):
"""
The default spelling policy for notes played back by this instrument.
See :class:`~scamp.instruments.ScampInstrument`
"""
return self.staff_parts[0].default_spelling_policy
@default_spelling_policy.setter
def default_spelling_policy(self, value: Union[SpellingPolicy, str]):
for instrument in self.staff_parts:
instrument.default_spelling_policy = value