Sampled Instrument Player with static and monolithic design. All instruments are built-in.
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.

603 lines
17 KiB

#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright, Nils Hilbricht, Germany ( https://www.hilbricht.net )
This code 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 re
from calfbox import cbox #use the globally installed calfbox
from asyncio import get_event_loop
from sys import stdout, maxsize
import os, signal
D1024 =210 * 2**0 # = 210. The lcm of 2, 3, 5, 7 . according to www.informatics.indiana.edu/donbyrd/CMNExtremes.htm this is the real world limit.
D512 = 210 * 2**1
D256 = 210 * 2**2
D128 = 210 * 2**3
D64 = 210 * 2**4
D32 = 210 * 2**5
D16 = 210 * 2**6 #16th 13440 ticks
D8 = 210 * 2**7 #eigth 26880 ticks
D4 = 210 * 2**8 #quarter 53760 ticks
D2 = 210 * 2**9 #half 107520 ticks
D1 = 210 * 2**10 #whole 215040 ticks
DB = 210 * 2**11 #brevis 430080 ticks
DL = 210 * 2**12 #longa
DM = 210 * 2**13 #maxima
#MAXIMUM = 0x7FFFFFFF # 31bit. maximum number of calfbox ticks allowed for its timeline, for example for the song duration
MAXIMUM = 100 * D1
#max_pulses = min(2**31, 2**31 * ppqn * bpm / (60 * sample_rate))
ly2pitch = {
"ceses,,," : 00,
"ces,,," : 10,
"c,,," : 20,
"cis,,," : 30,
"cisis,,," : 40,
"deses,,," : 50,
"des,,," : 60,
"d,,," : 70,
"dis,,," : 80,
"disis,,," : 90,
"eeses,,," : 100,
"ees,,," : 110,
"e,,," : 120,
"eis,,," : 130,
"eisis,,," : 140,
"feses,,," : 150,
"fes,,," : 160,
"f,,," : 170,
"fis,,," : 180,
"fisis,,," : 190,
"geses,,," : 200,
"ges,,," : 210,
"g,,," : 220,
"gis,,," : 230,
"gisis,,," : 240,
"aeses,,," : 250,
"aes,,," : 260,
"a,,," : 270,
"ais,,," : 280,
"aisis,,," : 290,
"beses,,," : 300,
"bes,,," : 310,
"b,,," : 320,
"bis,,," : 330,
"bisis,,," : 340,
"ceses,," : 350,
"ces,," : 360,
"c,," : 370,
"cis,," : 380,
"cisis,," : 390,
"deses,," : 400,
"des,," : 410,
"d,," : 420,
"dis,," : 430,
"disis,," : 440,
"eeses,," : 450,
"ees,," : 460,
"e,," : 470,
"eis,," : 480,
"eisis,," : 490,
"feses,," : 500,
"fes,," : 510,
"f,," : 520,
"fis,," : 530,
"fisis,," : 540,
"geses,," : 550,
"ges,," : 560,
"g,," : 570,
"gis,," : 580,
"gisis,," : 590,
"aeses,," : 600,
"aes,," : 610,
"a,," : 620,
"ais,," : 630,
"aisis,," : 640,
"beses,," : 650,
"bes,," : 660,
"b,," : 670,
"bis,," : 680,
"bisis,," : 690,
"ceses," : 700,
"ces," : 710,
"c," : 720,
"cis," : 730,
"cisis," : 740,
"deses," : 750,
"des," : 760,
"d," : 770,
"dis," : 780,
"disis," : 790,
"eeses," : 800,
"ees," : 810,
"e," : 820,
"eis," : 830,
"eisis," : 840,
"feses," : 850,
"fes," : 860,
"f," : 870,
"fis," : 880,
"fisis," : 890,
"geses," : 900,
"ges," : 910,
"g," : 920,
"gis," : 930,
"gisis," : 940,
"aeses," : 950,
"aes," : 960,
"a," : 970,
"ais," : 980,
"aisis," : 990,
"beses," : 1000,
"bes," : 1010,
"b," : 1020,
"bis," : 1030,
"bisis," : 1040,
"ceses" : 1050,
"ces" : 1060,
"c" : 1070,
"cis" : 1080,
"cisis" : 1090,
"deses" : 1100,
"des" : 1110,
"d" : 1120,
"dis" : 1130,
"disis" : 1140,
"eeses" : 1150,
"ees" : 1160,
"e" : 1170,
"eis" : 1180,
"eisis" : 1190,
"feses" : 1200,
"fes" : 1210,
"f" : 1220,
"fis" : 1230,
"fisis" : 1240,
"geses" : 1250,
"ges" : 1260,
"g" : 1270,
"gis" : 1280,
"gisis" : 1290,
"aeses" : 1300,
"aes" : 1310,
"a" : 1320,
"ais" : 1330,
"aisis" : 1340,
"beses" : 1350,
"bes" : 1360,
"b" : 1370,
"bis" : 1380,
"bisis" : 1390,
"ceses'" : 1400,
"ces'" : 1410,
"c'" : 1420,
"cis'" : 1430,
"cisis'" : 1440,
"deses'" : 1450,
"des'" : 1460,
"d'" : 1470,
"dis'" : 1480,
"disis'" : 1490,
"eeses'" : 1500,
"ees'" : 1510,
"e'" : 1520,
"eis'" : 1530,
"eisis'" : 1540,
"feses'" : 1550,
"fes'" : 1560,
"f'" : 1570,
"fis'" : 1580,
"fisis'" : 1590,
"geses'" : 1600,
"ges'" : 1610,
"g'" : 1620,
"gis'" : 1630,
"gisis'" : 1640,
"aeses'" : 1650,
"aes'" : 1660,
"a'" : 1670,
"ais'" : 1680,
"aisis'" : 1690,
"beses'" : 1700,
"bes'" : 1710,
"b'" : 1720,
"bis'" : 1730,
"bisis'" : 1740,
"ceses''" : 1750,
"ces''" : 1760,
"c''" : 1770,
"cis''" : 1780,
"cisis''" : 1790,
"deses''" : 1800,
"des''" : 1810,
"d''" : 1820,
"dis''" : 1830,
"disis''" : 1840,
"eeses''" : 1850,
"ees''" : 1860,
"e''" : 1870,
"eis''" : 1880,
"eisis''" : 1890,
"feses''" : 1900,
"fes''" : 1910,
"f''" : 1920,
"fis''" : 1930,
"fisis''" : 1940,
"geses''" : 1950,
"ges''" : 1960,
"g''" : 1970,
"gis''" : 1980,
"gisis''" : 1990,
"aeses''" : 2000,
"aes''" : 2010,
"a''" : 2020,
"ais''" : 2030,
"aisis''" : 2040,
"beses''" : 2050,
"bes''" : 2060,
"b''" : 2070,
"bis''" : 2080,
"bisis''" : 2090,
"ceses'''" : 2100,
"ces'''" : 2110,
"c'''" : 2120,
"cis'''" : 2130,
"cisis'''" : 2140,
"deses'''" : 2150,
"des'''" : 2160,
"d'''" : 2170,
"dis'''" : 2180,
"disis'''" : 2190,
"eeses'''" : 2200,
"ees'''" : 2210,
"e'''" : 2220,
"eis'''" : 2230,
"eisis'''" : 2240,
"feses'''" : 2250,
"fes'''" : 2260,
"f'''" : 2270,
"fis'''" : 2280,
"fisis'''" : 2290,
"geses'''" : 2300,
"ges'''" : 2310,
"g'''" : 2320,
"gis'''" : 2330,
"gisis'''" : 2340,
"aeses'''" : 2350,
"aes'''" : 2360,
"a'''" : 2370,
"ais'''" : 2380,
"aisis'''" : 2390,
"beses'''" : 2400,
"bes'''" : 2410,
"b'''" : 2420,
"bis'''" : 2430,
"bisis'''" : 2440,
"ceses''''" : 2450,
"ces''''" : 2460,
"c''''" : 2470,
"cis''''" : 2480,
"cisis''''" : 2490,
"deses''''" : 2500,
"des''''" : 2510,
"d''''" : 2520,
"dis''''" : 2530,
"disis''''" : 2540,
"eeses''''" : 2550,
"ees''''" : 2560,
"e''''" : 2570,
"eis''''" : 2580,
"eisis''''" : 2590,
"feses''''" : 2600,
"fes''''" : 2610,
"f''''" : 2620,
"fis''''" : 2630,
"fisis''''" : 2640,
"geses''''" : 2650,
"ges''''" : 2660,
"g''''" : 2670,
"gis''''" : 2680,
"gisis''''" : 2690,
"aeses''''" : 2700,
"aes''''" : 2710,
"a''''" : 2720,
"ais''''" : 2730,
"aisis''''" : 2740,
"beses''''" : 2750,
"bes''''" : 2760,
"b''''" : 2770,
"bis''''" : 2780,
"bisis''''" : 2790,
"ceses'''''" : 2800,
"ces'''''" : 2810,
"c'''''" : 2820,
"cis'''''" : 2830,
"cisis'''''" : 2840,
"deses'''''" : 2850,
"des'''''" : 2860,
"d'''''" : 2870,
"dis'''''" : 2880,
"disis'''''" : 2890,
"eeses'''''" : 2900,
"ees'''''" : 2910,
"e'''''" : 2920,
"eis'''''" : 2930,
"eisis'''''" : 2940,
"feses'''''" : 2950,
"fes'''''" : 2960,
"f'''''" : 2970,
"fis'''''" : 2980,
"fisis'''''" : 2990,
"geses'''''" : 3000,
"ges'''''" : 3010,
"g'''''" : 3020,
"gis'''''" : 3030,
"gisis'''''" : 3040,
"aeses'''''" : 3050,
"aes'''''" : 3060,
"a'''''" : 3070,
"ais'''''" : 3080,
"aisis'''''" : 3090,
"beses'''''" : 3100,
"bes'''''" : 3110,
"b'''''" : 3120,
"bis'''''" : 3130,
"bisis'''''" : 3140,
#"r" : float('inf'), a rest is not a pitch
}
def plain(pitch):
""" Extract the note from a note-number, without any octave but with the tailing zero.
This means we double-use the lowest octave as abstract version."""
#Dividing through the octave, 350, results in the number of the octave and the note as remainder.
return divmod(pitch, 350)[1]
def octave(pitch):
"""Return the octave of given note. Lowest 0 is X,,,"""
return divmod(pitch, 350)[0]
def halfToneDistanceFromC(pitch):
"""Return the half-tone step distance from C. The "sounding" interval"""
return {
#00 : 10, # ceses,,, -> bes
#10 : 11, # ces,,, -> b
00 : -2, # ceses,,, -> bes
10 : -1, # ces,,, -> b
20 : 0, # c,,,
30 : 1, # cis,,,
40 : 2, # cisis,,, -> d ...
50 : 0, # deses,,,
60 : 1, # des,,,
70 : 2, # d,,,
80 : 3, # dis,,,
90 : 4, # disis,,,
100 : 2, # eeses,,,
110 : 3, # ees,,,
120 : 4, # e,,,
130 : 5, # eis,,,
140 : 6, # eisis,,,
150 : 3, # feses,,,
160 : 4, # fes,,,
170 : 5, # f,,,
180 : 6, # fis,,,
190 : 7, # fisis,,,
200 : 5, # geses,,,
210 : 6, # ges,,,
220 : 7, # g,,,
230 : 8, # gis,,,
240 : 9, # gisis,,,
250 : 7, # aeses,,,
260 : 8, # aes,,,
270 : 9, # a,,,
280 : 10, # ais,,,
290 : 11, # aisis,,,
300 : 9, # beses,,,
310 : 10, # bes,,,
320 : 11, # b,,,
330 : 12, # bis,,,
340 : 13, # bisis,,,
#330 : 0, # bis,,,
#340 : 1, # bisis,,,
}[plain(pitch)]
lyToMidi = {} #filled for all pitches on startup, below
for ly, pitch in ly2pitch.items():
octOffset = (octave(pitch) +1) * 12 #twelve tones per midi octave
lyToMidi[ly] = octOffset + halfToneDistanceFromC(pitch)
lyToTicks = {
"16" : D16,
"8" : D8,
"4" : D4,
"2" : D2,
"1" : D1,
}
def ly(lilypondString):
"""Take string of simple lilypond notes, return midi pitches as generator of (pitch, ticks)"""
lastDur = "4"
for lyNote in lilypondString.split(" "):
try:
lyPitch, lyDur = re.split(r'(\d+)', lyNote)[0:2]
lastDur = lyDur
except ValueError:
lyPitch = re.split(r'(\d+)', lyNote)[0]
lyDur = lastDur
yield (lyToMidi[lyPitch], lyToTicks[lyDur])
def ly2cbox(lilypondString):
"""Return (pbytes, durationInTicks)
a python byte data type with midi data for cbox"""
#cbox.Pattern.serialize_event(position, midibyte1 (noteon), midibyte2(pitch), midibyte3(velocity))
pblob = bytes()
startTick = 0
for midiPitch, durationInTicks in ly(lilypondString):
endTick = startTick + durationInTicks - 1 #-1 ticks to create a small logical gap. This is nothing compared to our tick value dimensions, but it is enough for the midi protocol to treat two notes as separate ones. Imporant to say that this does NOT affect the next note on. This will be mathematically correct anyway.
pblob += cbox.Pattern.serialize_event(startTick, 0x90, midiPitch, 100) # note on
pblob += cbox.Pattern.serialize_event(endTick , 0x80, midiPitch, 100) # note off
startTick = startTick + durationInTicks #no -1 for the next note
return pblob, startTick
cboxTracks = {} #trackName:(cboxTrack,cboxMidiOutUuid)
def cboxSetTrack(trackName, durationInTicks, pattern):
"""Creates or resets calfbox tracks including jack connections
Keeps jack connections alive.
pattern is most likely a single pattern created through cbox.Document.get_song().pattern_from_blob
But it can also be a list of such patterns. In this case all patterns must be the same duration
and the parameter durationInTicks is the length of ONE pattern.
"""
if not trackName in cboxTracks:
cboxMidiOutUuid = cbox.JackIO.create_midi_output(trackName)
calfboxTrack = cbox.Document.get_song().add_track()
cboxTracks[trackName] = (calfboxTrack, cboxMidiOutUuid)
else:
calfboxTrack, cboxMidiOutUuid = cboxTracks[trackName]
calfboxTrack.delete()
calfboxTrack = cbox.Document.get_song().add_track()
calfboxTrack.set_external_output(cboxMidiOutUuid)
cbox.JackIO.rename_midi_output(cboxMidiOutUuid, trackName)
calfboxTrack.set_name(trackName)
if type(pattern) is cbox.DocPattern:
calfboxTrack.add_clip(0, 0, durationInTicks, pattern) #pos, offset, length(and not end-position, but is the same for the complete track), pattern
else: #iterable
assert iter(pattern)
#durationInTicks is the length of ONE pattern.
for i, pat in enumerate(pattern):
calfboxTrack.add_clip(i*durationInTicks, 0, durationInTicks, pat) #pos, offset, length, pattern.
calfboxTrack.add_clip(i*durationInTicks, 0, durationInTicks, pat) #pos, offset, length, pattern.
return calfboxTrack
def ly2Track(trackName, lyString):
"""Convert a simple string of lilypond notes to a cbox track and add that to the score"""
music = lyString
cboxBlob, durationInTicks = ly2cbox(music)
pattern = cbox.Document.get_song().pattern_from_blob(cboxBlob, durationInTicks)
cboxSetTrack(trackName, durationInTicks, pattern)
def getLongestTrackDuationInTicks():
return max( max(clip.pos + clip.length for clip in cboxTrack.track.status().clips) for cboxTrack in cbox.Document.get_song().status().tracks if cboxTrack.track.status().clips)
#for cboxTrack in cbox.Document.get_song().status().tracks:
# print (cboxTrack.track)
#TODO: These are different than the one above. It is more. Why?
#for cboxTrack, cboxMidiOutUuid in cboxTracks.values():
# print (cboxTrack.status())
def cboxLoop(eventLoop):
cbox.call_on_idle()
assert eventLoop.is_running()
#it is not that simple. status = "[Running]" if cbox.Transport.status().playing else "[Stopped]"
if cbox.Transport.status().playing == 1:
status = "[Running]"
elif cbox.Transport.status().playing == 0:
status = "[Stopped]"
elif cbox.Transport.status().playing == 2:
status = "[Stopping]"
elif cbox.Transport.status().playing is None:
status = "[Uninitialized]"
else:
raise ValueError("Unknown playback status: {}".format(cbox.Transport.status().playing))
stdout.write(" \r") #it is a hack but it cleans the line from old artefacts
stdout.write('{}: {}\r'.format(status, cbox.Transport.status().pos_ppqn))
stdout.flush()
eventLoop.call_later(0.1, cboxLoop, eventLoop) #100ms delay
eventLoop = get_event_loop()
def initCbox(clientName, internalEventProcessor=True, commonMidiInput=True):
cbox.init_engine("")
cbox.Config.set("io", "client_name", clientName)
cbox.Config.set("io", "enable_common_midi_input", commonMidiInput) #the default "catch all" midi input
cbox.start_audio()
scene = cbox.Document.get_engine().new_scene()
scene.clear()
cbox.do_cmd("/master/set_ppqn_factor", None, [D4]) #quarter note has how many ticks?
cbox.Transport.stop()
cbox.Transport.seek_ppqn(0)
cbox.Transport.set_tempo(120.0) #must be float
if internalEventProcessor:
eventLoop.call_soon(cboxLoop, eventLoop)
return scene, cbox, eventLoop
def connectPhysicalKeyboards(port="midi"):
midiKeyboards = cbox.JackIO.get_ports(".*", cbox.JackIO.MIDI_TYPE, cbox.JackIO.PORT_IS_SOURCE | cbox.JackIO.PORT_IS_PHYSICAL)
ourMidiInPort = cbox.Config.get("io", "client_name",) + ":" + port
for keyboard in midiKeyboards:
cbox.JackIO.port_connect(keyboard, ourMidiInPort)
def start(autoplay = False, userfunction = None, tempo = 120):
def ask_exit():
print()
eventLoop.stop()
shutdownCbox()
try:
dur = getLongestTrackDuationInTicks()
print("Starting with supplied music data. Setting sond duration to longest track")
assert dur > 0
cbox.Document.get_song().set_loop(dur, dur) #set playback length for the entire score.
except ValueError:
print ("Starting without a track. Setting song duration to a high value to generate recording space")
cbox.Document.get_song().set_loop(MAXIMUM, MAXIMUM) #set playback length for the entire score.
cbox.Transport.set_tempo(float(tempo))
cbox.Document.get_song().update_playback()
for signame in ('SIGINT', 'SIGTERM'):
eventLoop.add_signal_handler(getattr(signal, signame), ask_exit)
if userfunction:
print ("Send SIGUSR1 with following command to trigger user function")
print ("kill -10 {}".format(os.getpid()))
print ()
eventLoop.add_signal_handler(getattr(signal, "SIGUSR1"), userfunction)
print ("Use jack transport to control playback")
print ("Press Ctrl+C to abort")
print("pid %s: send SIGINT or SIGTERM to exit." % os.getpid())
try:
eventLoop.run_forever()
finally:
eventLoop.close()
def shutdownCbox():
cbox.Transport.stop()
cbox.Transport.seek_ppqn(0)
cbox.stop_audio()
cbox.shutdown_engine()