You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

127 lines
5.8 KiB

#! /usr/bin/env python3
# -*- coding: utf-8 -*-
Copyright 2022, Nils Hilbricht, Germany ( )
This file is part of the Laborejo Software Suite ( ),
This application 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
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 <>.
import logging; logger = logging.getLogger(__name__);"import")
#Standard Library Modules
from typing import Tuple
import os.path
#Third Party Modules
from template.calfbox import cbox
#Our template modules
from ..start import PATHS
from . import sequencer
from ..helper import cache_unlimited, flatList
from .duration import D1024 #to add a note off spacer.
class Metronome(object):
A metronome uses calfbox to generate a click track.
All calfbox handling and midi generation are internally.
The metronome has multiple components, each can be switched on and off on creation:
-stereo audio out (stereo for cbox reasons)
-midi out
-midi in
You can configure the midi notes representing a stressed and normal tick.
There is no half-stressed signal.
Stressing can be switched off.
The metronome is a real midi track, not generated on the fly. Therefore it needs to be set
with new data when music changes, which happens quite often. In general you want you metronome
to be as long as your song.
You can choose to loop the last measure, which makes simple "give me 4/4 at 120" possible.
def __init__(self, parentData, normalMidiNote=77, stressedMidiNote=76, midiChannel=9):
self.parentData = parentData
#self.sequencerInterface = sequencer.SequencerInterface(parentTrack=self, name="metronome")
self.sfzInstrumentSequencerInterface = sequencer.SfzInstrumentSequencerInterface(parentTrack=self, name="metronome", absoluteSfzPath=os.path.join(PATHS["templateShare"], "metronome", "metronome.sfz"))
#testing: self.sfzInstrumentSequencerInterface = sequencer.SequencerInterface(parentTrack=self, name="metronome"); self.sfzInstrumentSequencerInterface.setEnabled(True) needs activating below!!!
self._soundStresses = True #Change through soundStresses function
self._normalTickMidiNote = normalMidiNote #no changing after instance got created
self._stressedTickMidiNote = stressedMidiNote #no changing after instance got created
self._midiChannel = midiChannel #GM drums #1-16 #no changing after instance got created
self._cachedData = None #once we have a track it gets saved here so the midi output can be regenerated in place.
self.label = "" #E.g. current Track Name, but can be anything.
self.setEnabled(False) #TODO: save load
def soundStresses(self, value:bool):
self._soundStresses = value
self.generate(self._cachedData, self.label)
def generate(self, data, label:str):
"""Data is ordered: Iterable of (positionInTicks, isMetrical, treeOfMetricalInstructions)
as tuple. Does not check if we truly need an update
Label typically is the track name"""
assert not data is None
self.label = label
self._cachedData = data
result = []
for position, isMetrical, treeOfMetricalInstructions in data:
isMetrical = False if not self._soundStresses else True
blob, length = self.instructionToCboxMeasure(isMetrical, treeOfMetricalInstructions)
result.append((blob, position, length))
#we skip over instructions which have no proto-measure, metrical or not. This basically creates a zone without a metronome.
#This might be difficult to use when thinking of Laborejo alone but in combination with a real time audio recording in another program this becomes very useful.
#TODO: repeat last instruction as loop
def instructionToCboxMeasure(self, isMetrical, metricalInstruction:tuple)->Tuple[bytes,int]:
"""Convert a metrical instruction to a metronome measure"""
measureBlob = bytes()
workingTicks = 0
ranOnce = False
for duration in flatList(metricalInstruction):
if ranOnce or not isMetrical: #normal tick. Always normal if this measure has no stressed positions, in other words it is not metrical
measureBlob += cbox.Pattern.serialize_event(workingTicks, 0x90+9, 77, 127) #full velocity
measureBlob += cbox.Pattern.serialize_event(workingTicks+D1024, 0x80+9, 77, 127) #note off is the shortest value possible in this program.
else: #Measure "one"
measureBlob += cbox.Pattern.serialize_event(workingTicks, 0x90+9, 76, 127) #different pitch
measureBlob += cbox.Pattern.serialize_event(workingTicks+D1024, 0x80+9, 76, 127)
ranOnce = True
workingTicks += duration
return (measureBlob, workingTicks)
def enabled(self)->bool:
return self.sfzInstrumentSequencerInterface.enabled
def setEnabled(self, value:bool):
def export(self)->dict:
return {
# "sequencerInterface" : self.sequencerInterface.export(),
"enabled" : self.enabled,
"label" : self.label,