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.

128 lines
5.8 KiB

#! /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 ),
5 years ago
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.
3 years ago
The metronome has multiple components, each can be switched on and off on creation:
3 years ago
-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.
3 years ago
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.
3 years ago
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
3 years ago
#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!!!
3 years ago
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.
3 years ago
self.setEnabled(False) #TODO: save load
def soundStresses(self, value:bool):
self._soundStresses = value
3 years ago
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
3 years ago
Label typically is the track name"""
assert not data is None
self.label = label
3 years ago
self._cachedData = data
result = []
for position, isMetrical, treeOfMetricalInstructions in data:
isMetrical = False if not self._soundStresses else True
3 years ago
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
3 years ago
@cache_unlimited
def instructionToCboxMeasure(self, isMetrical, metricalInstruction:tuple)->Tuple[bytes,int]:
"""Convert a metrical instruction to a metronome measure"""
measureBlob = bytes()
workingTicks = 0
3 years ago
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
3 years ago
workingTicks += duration
return (measureBlob, workingTicks)
3 years ago
@property
def enabled(self)->bool:
3 years ago
return self.sfzInstrumentSequencerInterface.enabled
def setEnabled(self, value:bool):
self.sfzInstrumentSequencerInterface.enable(value)
3 years ago
def export(self)->dict:
return {
# "sequencerInterface" : self.sequencerInterface.export(),
"enabled" : self.enabled,
"label" : self.label,
3 years ago
}