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.
129 lines
6.0 KiB
129 lines
6.0 KiB
#! /usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Copyright 2020, 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 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)
|
|
|
|
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,
|
|
}
|
|
|
|
|
|
|