#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright 2022 , Nils Hilbricht , Germany ( https : / / www . hilbricht . net )
This file is part of the Laborejo Software Suite ( https : / / www . laborejo . org ) ,
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
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 / > .
"""
import logging ; logger = logging . getLogger ( __name__ ) ; logger . info ( " 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 getPortNames ( self ) - > ( str , str ) :
""" Return two client:port , for left and right channel """
return ( self . sfzInstrumentSequencerInterface . portnameL , self . sfzInstrumentSequencerInterface . portnameR )
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 ) )
self . sfzInstrumentSequencerInterface . setTrack ( result )
#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
@cache_unlimited
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 )
@property
def enabled ( self ) - > bool :
return self . sfzInstrumentSequencerInterface . enabled
def setEnabled ( self , value : bool ) :
self . sfzInstrumentSequencerInterface . enable ( value )
def export ( self ) - > dict :
return {
# "sequencerInterface" : self.sequencerInterface.export(),
" enabled " : self . enabled ,
" label " : self . label ,
}