Components#

class bmusic.Animator(obj: Object, data_path: str, index: int = 0, action_name: str | None = None)#

Wrapper around Blender’s animation API, providing control of keyframe type and handles.

Usage example:

anim = Animator(obj, "location", index=1)   # Y location
anim.animate(0, 0)
anim.animate(10, 10, handle="VECTOR")
anim.animate(20, 20, type="EXTREME")
animate(frame: float, value: Any, handle: str = 'AUTO_CLAMPED', type: str = 'KEYFRAME')#

Insert keyframe.

obj: Object#

Blender object to animate.

class bmusic.AnimKey(animators: List[Animator], basis: Sequence[float])#

Set of value presets for animators.

Each key is a list of values corresponding to each animator. The basis key is always defined at initialize. Other keys are diffs with respect to the basis key.

Animation is done by assigning weights to each key.

Usage example:

anim1 = bmusic.Animator(obj, "location", 0)
anim2 = bmusic.Animator(obj, "location", 1)
anim3 = bmusic.Animator(obj, "location", 2)

key = bmusic.AnimKey([anim1, anim2, anim3], [0, 0, 0])
key["up"] = (0, 0, 1)
key["cool"] = (1, 1, -1)
key.animate(0)
key.animate(30, up=2, handle="VECTOR", type="JITTER")
key.animate(60, up=1, cool=2, type="EXTREME")
animate(frame: float, handle: str = 'AUTO_CLAMPED', type: str = 'KEYFRAME', **kwargs)#

Animate all animators.

Parameters:

kwargs – Weight of each shape key.

class bmusic.MessageList(messages: Sequence[Message] | None = None)#

Sequence of messages sorted by start time.

A frozen class; that is, cannot be modified after creation.

TODO this shouldn’t be frozen. It should be possible to add and remove messages.

duration() float#

Duration, in frames, between first msg’s start and last msg’s end.

filter_notes(notes: Sequence[int]) MessageList#

Only keep messages with note in the given list.

noteset()#

Set of all notes used in this track.

split_notes()#

Split into multiple tracks, each containing only one note.

class bmusic.Message(note, velocity, start, end, msglist: MessageList | None = None, index: int | None = None)#

Message with absolute start and end time in frames. Also supports linked-list like behavior within track.

diff(other: Message) float#

Calculate difference in frames between this message and another’s start. Positive if this message starts after other.

next() Message | None#

Get next message in msglist.

next_start(fallback: float = 1000000000.0) float#

Timestamp of next message’s start.

Parameters:

fallback – Value to return if no next message.

prev() Message | None#

Get previous message in msglist.

prev_end(fallback: float = -1000000000.0) float#

Timestamp of previous message’s end.

Parameters:

fallback – Value to return if no previous message.

prev_start(fallback: float = -1000000000.0) float#

Timestamp of previous message’s start.

Parameters:

fallback – Value to return if no previous message.

class bmusic.AffixMessage(note, velocity, start, end, msglist: MessageList | None = None, index: int | None = None)#

Bases: Message

Prefix and suffix extension.

Based on application, suffix is either time after start or end. This is set by suffix_after_end in compute_affixes().

prefix: float#

Length of prefix in frames.

suffix: float#

Length of suffix in frames.

bmusic.compute_affixes(track: MessageList, split: float = 0.5, max_prefix: float = 1000000000.0, max_suffix: float = 1000000000.0, suffix_after_end: bool = False, hard_end: bool = False) list[AffixMessage]#

For each message (taking into account neighboring messages in the track), compute length of prefix and suffix.

For example, a prefix may be a hammer anticipating, and a suffix may be the bounce back.

Prefixes and suffixes don’t overlap with the next message — they will be shortened if next/prev note is too close.

The result may look like this:

Messages:    |--------|-----------|--|--|--------|
Return:    <<|>>>---<<|>>>------<<|><|><|>>>---<<|>>>

Each vertical line represents when each message plays. On the line marked Return, < shows the duration of prefix and > for suffix.

Notice how the max prefix length is 2, and the max suffix length is 3.

Also notice how the prefix and suffix don’t overlap with the next message — they are shortened when necessary.

If we take this result and interpret prefixes and suffixes as hammer actions as described above, this would create a smooth-looking animation.

See the docs page for more details.

Parameters:
  • track – Track to process.

  • split – Ratio between prefix and suffix when they overlap. 0 means completely prefix, and 1 means completely suffix.

  • max_prefix – Maximum length of prefix in frames.

  • max_suffix – Maximum length of suffix in frames.

  • suffix_after_end – If true, suffix is after message ends. Else, suffix is immediately after message starts.

  • hard_end – If true, cut off suffix when message ends (e.g. stop glowing when note is released). Otherwise, extend until max_suffix (e.g. keep glowing indefinitely). Naturally, if used with suffix_after_end, suffix will be 0.

Returns:

List of Affix

bmusic.split_chords(midi: MessageList, threshold: float) list[list[Message]]#

Split a MIDI track into chords — messages that play at the same time (or within a threshold of each other).

Messages less than threshold apart will be combined into one chord.

Threshold units are whatever given MessageList units are; most likely frames.