commit 778fe31c3814f5576fcff9c79b93562930dbf17e Author: Pascal Engélibert Date: Thu Feb 27 00:41:31 2025 +0100 Initial commit diff --git a/fugue1.mid b/fugue1.mid new file mode 100644 index 0000000..db1a26d Binary files /dev/null and b/fugue1.mid differ diff --git a/level1.py b/level1.py new file mode 100644 index 0000000..659f9e0 --- /dev/null +++ b/level1.py @@ -0,0 +1,11 @@ +""" +Niveau 1 +Jouer des sons +""" + +import synthlib, math + +sound_encoder = synthlib.SoundEncoder() +for i in range(13): + for t in range(4000): + sound_encoder.write_sample(math.sin(t/16000*440*2**(i/12)*2*math.pi)) diff --git a/level2.py b/level2.py new file mode 100644 index 0000000..aa0df43 --- /dev/null +++ b/level2.py @@ -0,0 +1,18 @@ +""" +Niveau 2 +Jouer une mélodie +""" + +import synthlib, math + +music = [ + (1, 0), + (1, 2), + (2, 4) +] + +sound_encoder = synthlib.SoundEncoder() +for note in music: + for t in range(note[0]*4000): + sound_encoder.write_sample(math.sin(t/16000*440*2**(note[1]/12)*2*math.pi)) + diff --git a/level3.py b/level3.py new file mode 100644 index 0000000..cc5c363 --- /dev/null +++ b/level3.py @@ -0,0 +1,22 @@ +""" +Niveau 3 +Jouer des accords + +Ne pas introduire le facteur d'amplitude avant d'avoir entendu le résultat sans lui +""" + +import synthlib, math + +music = [ + (1, [-9]), + (1, [-5]), + (1, [-2]), + (3, [-9, -5, -2]) +] + +sound_encoder = synthlib.SoundEncoder() +for note in music: + for t in range(note[0]*6000): + sound_encoder.write_sample(sum([0.3*math.sin(t/16000*440*2**(i/12)*2*math.pi) for i in note[1]])) + + diff --git a/level4.py b/level4.py new file mode 100644 index 0000000..7808444 --- /dev/null +++ b/level4.py @@ -0,0 +1,42 @@ +""" +Niveau 4 +Jouer des accords + +Ne pas introduire le facteur d'amplitude avant d'avoir entendu le résultat sans lui +""" + +import synthlib, math, sys + +music = synthlib.midi() + +active = {} +wait = 0 +tt = 0 +t = 0 +loop = True + +sound_encoder = synthlib.SoundEncoder() +while loop: + if wait == 0: + while True: + event = music[tt] + tt += 1 + if event.type == synthlib.Note: + if event.velocity == 0: + if event.number in active: + active.pop(event.number) + sys.stderr.write("pop "+str(event.number)+"\n") + else: + active[event.number] = event.velocity + sys.stderr.write(str(event.velocity)+" "+str(event.number)+" ("+str(len(active))+")\n") + elif event.type == synthlib.Wait: + wait = int(event.value*8000) + break + elif event.type == synthlib.End: + loop = False + break + else: + wait -= 1 + + sound_encoder.write_sample(sum([active[number]*math.sin(t/16000*440*2**(number/12)*2*math.pi) for number in active])/max(1,len(active))) + t += 1 diff --git a/level5.py b/level5.py new file mode 100644 index 0000000..6abd981 --- /dev/null +++ b/level5.py @@ -0,0 +1,70 @@ +""" +Niveau 5 +Nouveaux instruments +""" + +import synthlib, math, sys + +rate = 44100 + +def sine(t, f): + return math.sin(t/rate*f*2*math.pi) + +def cosine(t, f): + return math.cosin(t/rate*f*2*math.pi) + +def sine2(t, f): + return math.atan(math.sin(t/rate*f*2*math.pi)) + +def sine3(t, f): + return (sine(t,f) - sine2(t,f))*math.pi + +def sine4(t, f): + return (cosine(t,f) - sine3(t,f))*2 + +polynome = [32, 30, 8, 4, 2, 1] +lpolynome = len(polynome) +spolynome = sum(polynome) +def polynomial(t, f): + return sum(j*math.sin(t/rate*f*2*math.pi*i) for i,j in enumerate(polynome)) / spolynome + +music = synthlib.midi() + +active = {} +wait = 0 +tt = 0 +t = 0 +loop = True + +sound_encoder = synthlib.SoundEncoder(env={ + "sampling": rate, + "signed": True, + "verbose": False, + "bits": 32, + "msbfirst": True, + "tempo": 130 +}) +while loop: + if wait == 0: + while True: + event = music[tt] + tt += 1 + if event.type == synthlib.Note: + if event.velocity == 0: + if event.number in active: + active.pop(event.number) + else: + active[event.number] = event.velocity + #sys.stderr.write(str(event.velocity)+"\n") + elif event.type == synthlib.Wait: + wait = int(event.value*rate/2) + break + elif event.type == synthlib.End: + loop = False + break + else: + wait -= 1 + + sound_encoder.write_sample(sum([active[number]*polynomial(t, 440*2**(number/12)) for number in active])/max(1,len(active))) + t += 1 + diff --git a/loop.py b/loop.py new file mode 100644 index 0000000..1e51d38 --- /dev/null +++ b/loop.py @@ -0,0 +1,29 @@ +import synthlib, math, sys + +rate = 44100 + +# freq is multiplied by factor every period +def sine(t, period, decay, factor, full_period): + tt = (t + decay) % full_period + if tt == 0: + volume = 0 + elif tt <= full_period/2: + volume = tt/(full_period/2) + else: + volume = (full_period-tt)/(full_period/2) + freq_factor = 27.5*factor**(tt/period) + return volume**2*math.sin(tt*freq_factor/rate*2*math.pi) + +nb = 3 + +sound_encoder = synthlib.SoundEncoder(env={ + "sampling": rate, + "signed": True, + "verbose": False, + "bits": 32, + "msbfirst": True, + "tempo": 130 +}) +for t in range(rate*16): + sound_encoder.write_sample(sum([sine(t, rate, i*rate/nb*4, 2, rate*4) for i in range(nb)])/nb) + diff --git a/mario.mid b/mario.mid new file mode 100644 index 0000000..c273142 Binary files /dev/null and b/mario.mid differ diff --git a/pub/level1.py b/pub/level1.py new file mode 100644 index 0000000..1b69151 --- /dev/null +++ b/pub/level1.py @@ -0,0 +1,13 @@ +""" +Niveau 1 +Jouer un son +""" + +# Permet d'utiliser les fonctions audio +import synth + +# crée un encodeur audio +encoder = synth.Encoder() + +# écrit un échantillon +encoder.write(0) diff --git a/pub/level2.py b/pub/level2.py new file mode 100644 index 0000000..a8e7c41 --- /dev/null +++ b/pub/level2.py @@ -0,0 +1,8 @@ +""" +Niveau 2 +Jouer des notes +""" + +import synth + +encoder = synth.Encoder() diff --git a/pub/level3.py b/pub/level3.py new file mode 100644 index 0000000..2d4c4d3 --- /dev/null +++ b/pub/level3.py @@ -0,0 +1,21 @@ +""" +Niveau 3 +Jouer une mélodie +""" + +import synth + +encoder = synth.Encoder() + +# Liste de notes, sous la forme [durée, note] +music = [ + [1, 4], + [1, 3], + [1, 4], + [1, 3], + [1, 4], + [1, -1], + [1, 2], + [1, 0], + [2, -3] +] diff --git a/pub/level4.py b/pub/level4.py new file mode 100644 index 0000000..8f0ea6f --- /dev/null +++ b/pub/level4.py @@ -0,0 +1,18 @@ +""" +Niveau 4 +Jouer des accords +""" + +import synth + +encoder = synth.Encoder() + +# Liste d'accords, sous la forme [durée, [note, note, note, ...]] +music = [ + [2, [5, 9]], + [2, [4, 8]], + [1, [-12]], + [1, [0, 2, 4]], + [1, [-10]], + [1, [2, 4, 6]], +] diff --git a/pub/level5.py b/pub/level5.py new file mode 100644 index 0000000..e21a155 --- /dev/null +++ b/pub/level5.py @@ -0,0 +1,13 @@ +""" +Niveau 5 +Jouer une partition +""" + +import synth + +encoder = synth.Encoder() + +music = synth.Midi() + +#for notes in music: + # notes est une liste dont les éléments sont [piste, note, force] diff --git a/pub/level6.py b/pub/level6.py new file mode 100644 index 0000000..2e10084 --- /dev/null +++ b/pub/level6.py @@ -0,0 +1,13 @@ +""" +Niveau 6 +Appliquer des filtres +* Écho : garder une copie du son et la jouer en décalé et moins fort +* Wah-wah : atténuer le son périodiquement +* Phaser : comme l'écho, mais en rejouant périodiquement des échantillons plus ou moins récents +""" + +import synth + +encoder = synth.Encoder() + +music = synth.Midi() diff --git a/pub/synth.py b/pub/synth.py new file mode 100644 index 0000000..720e170 --- /dev/null +++ b/pub/synth.py @@ -0,0 +1,237 @@ +#!/usr/bin/python3 + +import sys, os, subprocess, mido + +def getarg(pos, default): + if len(sys.argv) > pos: + return sys.argv[pos] + return default + +class Encoder: + def __init__(self, fp=None, fp_log=sys.stderr): + self.fp = fp if fp == None or fp.mode == "wb" else os.fdopen(fp.fileno(), 'wb') + self.fp_log = fp_log + + self.sampling = 16000 + self.signed = True + self.verbose = False + self.bits = 16 + self.msbfirst = True + + def write(self, amplitude): + if self.fp == None: + aplay = subprocess.Popen([ + "aplay", + "-r", + str(self.sampling), + "-f", + ("S" if self.signed else "U") + str(self.bits) + "_" + ("LE" if self.msbfirst else "BE") + ], stdin=subprocess.PIPE, stderr=self.fp_log) + self.fp = aplay.stdin + + smax = 2**(self.bits-self.signed)-1 + if self.signed: + v = int(amplitude*smax) + w = v + s = v < 0 + v = abs(v) + if s: + v = smax-v + v |= s << (self.bits-1) + else: + v = int((amplitude*smax+smax)/2) + w = v + + if self.msbfirst: + i = 0 + while i < self.bits: + if self.fp.write(bytes([(v>>i)&255])) > 1: + self.fp_log.write(str(v)) + if self.verbose: + self.fp_log.write(bin((v>>i)&255)[2:].zfill(8)) + i += 8 + else: + i = self.bits + while i > 0: + i -= 8 + self.fp.write(bytes([(v>>i)&255])) + if self.verbose: + self.fp_log.write(bin((v>>i)&255)[2:].zfill(8)) + + if self.verbose: + self.fp_log.write(" "+"0"*(self.bits-len(bin(v))+2)+bin(v)[2:]+" "+str(v)+" "+str(w)+"\n") + +class Event: + def __init__(self, type): + self.type = type + +class Wait(Event): + def __init__(self, value): + Event.__init__(self, Wait) + self.value = value + + def dict(self): + return { + "type": "wait", + "number": self.value, + } + +class End(Event): + def __init__(self): + Event.__init__(self, End) + + def dict(self): + return { + "type": "end" + } + +class Note(Event): + def __init__(self, number, velocity, channel=0, track=0): + Event.__init__(self, Note) + self.number = number + self.velocity = velocity + self.channel = channel + self.track = track + + def dict(self): + return { + "type": "note", + "number": self.number, + "velocity": self.velocity, + "channel": self.channel + } + +def midi_events(): + if len(sys.argv) < 2: + raise ValueError("Il faut donner le nom du fichier: python3 "+sys.argv[0]+" fichier.mid") + return read_midi(sys.argv[1], getarg(2, 130)) + +_midi = None +_midi_active = {} +_midi_wait = 0 +_midi_tt = 0 +_midi_loop = True +def midi2(): + global _midi, _midi_active, _midi_wait, _midi_tt, _midi_loop + if _midi == None: + _midi = midi_events() + + if _midi_loop: + if _midi_wait == 0: + while True: + event = _midi[_midi_tt] + _midi_tt += 1 + if event.type == Note: + if event.velocity == 0: + if (event.number, event.track) in _midi_active: + _midi_active.pop((event.number, event.track)) + else: + _midi_active[(event.number, event.track)] = event.velocity + elif event.type == Wait: + _midi_wait = int(event.value*8000) + break + elif event.type == End: + _midi_loop = False + break + else: + _midi_wait -= 1 + + return [[idx[0], idx[1], _midi_active[idx]] for idx in _midi_active] + +""" +Lit le fichier MIDI donné en argument de la commande. +Retourne, pour chaque échantillon, la liste des notes sous la forme [piste, note, amplitude]. +""" +class Midi: + def __init__(self, sampling=16000): + self.sampling = sampling + + def __iter__(self): + self.active = {} + self.wait = 0 + self.tt = 0 + self.loop = True + self.events = midi_events() + return self + + def __next__(self): + if self.loop: + if self.wait == 0: + while True: + event = self.events[self.tt] + self.tt += 1 + if event.type == Note: + if event.velocity == 0: + if (event.track, event.number) in self.active: + self.active.pop((event.track, event.number)) + else: + sys.stderr.write("Piste="+str(event.track)+" Note="+str(event.number)+" Force="+str(event.velocity)+"\n") + self.active[(event.track, event.number)] = event.velocity + elif event.type == Wait: + self.wait = int(event.value*self.sampling/2) + break + elif event.type == End: + self.loop = False + break + else: + self.wait -= 1 + + return [[idx[0], idx[1], self.active[idx]] for idx in self.active] + raise StopIteration + +def read_midi(filename, tempo): + mid = mido.MidiFile(filename) + result = [] + tracks = [0 for i in mid.tracks] + tempo = [tempo for i in mid.tracks] + wait = [None for i in mid.tracks] + end = [False for i in mid.tracks] + t = 0 + loop = True + while loop: + loop = False + + # Find the first events + first = [] + firstt = None + for i, track in enumerate(mid.tracks): + if end[i]: + continue + if wait[i] == None: + wait[i] = track[tracks[i]].time/tempo[i]/4 + + if firstt == None or wait[i] < firstt: + firstt = wait[i] + first = [i] + elif wait[i] == firstt: + first.append(i) + + if firstt > 0: + result.append(Wait(firstt)) + + # Play the first events + for i, track in enumerate(mid.tracks): + if end[i]: + continue + loop = True + msg = track[tracks[i]] + if i in first: + # Play event + if msg.type == "set_tempo": + tempo[i] = 6e7/msg.tempo + elif msg.type == "note_on": + result.append(Note(msg.note-50, msg.velocity/100, msg.channel, i)) + elif msg.type == "end_of_track": + result.append(End()) + end[i] = True + + # Find next event + tracks[i] += 1 + wait[i] = None + else: + wait[i] -= firstt + + if not (False in end): + loop = False + + return result diff --git a/pub/tutorial.pdf b/pub/tutorial.pdf new file mode 100644 index 0000000..ec61dc9 Binary files /dev/null and b/pub/tutorial.pdf differ diff --git a/reunion.mid b/reunion.mid new file mode 100644 index 0000000..c23643e Binary files /dev/null and b/reunion.mid differ diff --git a/sheet b/sheet new file mode 100644 index 0000000..68db111 --- /dev/null +++ b/sheet @@ -0,0 +1,4 @@ +Activer le son: +amixer set Master 1+ toggle + +python level5.py tetris2.mid | aplay -r 44100 -f S32_LE diff --git a/stage/__pycache__/synth.cpython-311.pyc b/stage/__pycache__/synth.cpython-311.pyc new file mode 100644 index 0000000..6385efe Binary files /dev/null and b/stage/__pycache__/synth.cpython-311.pyc differ diff --git a/stage/level1.py b/stage/level1.py new file mode 100644 index 0000000..ecb88e1 --- /dev/null +++ b/stage/level1.py @@ -0,0 +1,11 @@ +""" +Niveau 1 +Jouer un son +""" + +import synth + +encoder = synth.Encoder() +for t in range(64000): + #encoder.write(math.sin(t/16000*400)) + encoder.write(t/16000*400%1) diff --git a/stage/level2.py b/stage/level2.py new file mode 100644 index 0000000..2ff7887 --- /dev/null +++ b/stage/level2.py @@ -0,0 +1,12 @@ +""" +Niveau 2 +Jouer des notes +""" + +import synth +import math + +encoder = synth.Encoder() +for note in range(13): + for t in range(16000): + encoder.write(math.sin(t/16000 * 220 * 2**(note/12) * 2*math.pi)) diff --git a/stage/level3.py b/stage/level3.py new file mode 100644 index 0000000..af34a9d --- /dev/null +++ b/stage/level3.py @@ -0,0 +1,25 @@ +""" +Niveau 3 +Jouer une mélodie +""" + +import synth +import math + +music = [ + [1, 4], + [1, 3], + [1, 4], + [1, 3], + [1, 4], + [1, -1], + [1, 2], + [1, 0], + [2, -3] +] + +encoder = synth.Encoder() +for note in music: + for t in range(note[0]*3000): + encoder.write(math.sin(t/16000 * 440 * 2**(note[1]/12) * 2*math.pi)) + diff --git a/stage/level4.py b/stage/level4.py new file mode 100644 index 0000000..f6d15d4 --- /dev/null +++ b/stage/level4.py @@ -0,0 +1,25 @@ +""" +Niveau 4 +Jouer des accords +""" + +import synth +import math + +music = [ + [2, [5, 9]], + [2, [4, 8]], + [1, [-12]], + [1, [0, 2, 4]], + [1, [-10]], + [1, [2, 4, 6]], +] + +encoder = synth.Encoder() + +for chord in music: + for t in range(chord[0]*4000): + total = 0 + for note in chord[1]: + total = total + math.sin(t/16000 * 440 * 2**(note/12) * 2*math.pi) + encoder.write(total * 0.3) diff --git a/stage/level5.py b/stage/level5.py new file mode 100644 index 0000000..13a15f9 --- /dev/null +++ b/stage/level5.py @@ -0,0 +1,27 @@ +""" +Niveau 5 +Jouer une partition +""" + +import synth +import math + +music = synth.Midi(32000) + +encoder = synth.Encoder() +encoder.sampling = 32000 + +instruments = [ + lambda t, f: math.sin(t * f * 2*math.pi), + lambda t, f: math.sin(t * f * 2*math.pi) + math.sin(t * f * 4*math.pi)/2, + lambda t, f: math.sin(t * f * 2*math.pi) + math.sin(t * f * 4*math.pi)/2, + lambda t, f: t * f % 1 - 0.5, +] + +t = 0 +for notes in music: + total = 0 + for note in notes: + total = total + instruments[note[0]](t/32000, 440 * 2**(note[1]/12)) * note[2] + encoder.write(total * 0.3) + t = t + 1 diff --git a/stage/level6.py b/stage/level6.py new file mode 100644 index 0000000..5678bcd --- /dev/null +++ b/stage/level6.py @@ -0,0 +1,48 @@ +""" +Niveau 6 +Filtre audio +""" + +import synth +import math +import random + +music = synth.Midi(32000) + +encoder = synth.Encoder() +encoder.sampling = 32000 + +instruments = [ + lambda t, f: round(math.sin(t * f * 2*math.pi)) + random.random() * 0.3, + lambda t, f: math.sin(t * f * 2*math.pi), + lambda t, f: math.sin(t * f * 2*math.pi)*0.6 + math.sin(t * f * 4*math.pi)*0.3 + math.sin(t * f * 8*math.pi)*0.1, + lambda t, f: math.sin(t * f * 2*math.pi) + math.sin(t * f * 4*math.pi)/2, + lambda t, f: t * f % 1 - 0.5, +] + +delay1 = [0] * 1000 +delay2 = [0] * 3000 +delay3 = [0] * 5000 +phaser = [0] * 50 + +t = 0 +for notes in music: + total = 0 + for note in notes: + total = total + instruments[note[0]](t/32000, 440 * 2**(note[1]/12)) * note[2] + + total = total * (math.sin(t/32000 * 20 * 2*math.pi)/8+7/8) + total += delay1.pop(0) * 0.1 + total += delay2.pop(0) * 0.3 + total += delay3.pop(0) * 0.4 + delay1.append(total) + delay2.append(total) + delay3.append(total) + + lfo = math.sin(t/32000 * 0.2 * 2*math.pi) + phaser.append(total) + phaser.pop(0) + total += phaser[int((lfo/2+0.5)*(len(phaser)-1))] + + encoder.write(total * 0.1) + t = t + 1 diff --git a/stage/synth.py b/stage/synth.py new file mode 100644 index 0000000..aca2d7c --- /dev/null +++ b/stage/synth.py @@ -0,0 +1,237 @@ +#!/usr/bin/python3 + +import sys, os, subprocess, mido + +def getarg(pos, default): + if len(sys.argv) > pos: + return sys.argv[pos] + return default + +class Encoder: + def __init__(self, fp=None, fp_log=sys.stderr): + self.fp = fp if fp == None or fp.mode == "wb" else os.fdopen(fp.fileno(), 'wb') + self.fp_log = fp_log + + self.sampling = 16000 + self.signed = True + self.verbose = False + self.bits = 16 + self.msbfirst = True + + def write(self, amplitude): + if self.fp == None: + aplay = subprocess.Popen([ + "aplay", + "-r", + str(self.sampling), + "-f", + ("S" if self.signed else "U") + str(self.bits) + "_" + ("LE" if self.msbfirst else "BE") + ], stdin=subprocess.PIPE, stderr=self.fp_log) + self.fp = aplay.stdin + + smax = 2**(self.bits-self.signed)-1 + if self.signed: + v = int(amplitude*smax) + w = v + s = v < 0 + v = abs(v) + if s: + v = smax-v + v |= s << (self.bits-1) + else: + v = int((amplitude*smax+smax)/2) + w = v + + if self.msbfirst: + i = 0 + while i < self.bits: + if self.fp.write(bytes([(v>>i)&255])) > 1: + self.fp_log.write(str(v)) + if self.verbose: + self.fp_log.write(bin((v>>i)&255)[2:].zfill(8)) + i += 8 + else: + i = self.bits + while i > 0: + i -= 8 + self.fp.write(bytes([(v>>i)&255])) + if self.verbose: + self.fp_log.write(bin((v>>i)&255)[2:].zfill(8)) + + if self.verbose: + self.fp_log.write(" "+"0"*(self.bits-len(bin(v))+2)+bin(v)[2:]+" "+str(v)+" "+str(w)+"\n") + +class Event: + def __init__(self, type): + self.type = type + +class Wait(Event): + def __init__(self, value): + Event.__init__(self, Wait) + self.value = value + + def dict(self): + return { + "type": "wait", + "number": self.value, + } + +class End(Event): + def __init__(self): + Event.__init__(self, End) + + def dict(self): + return { + "type": "end" + } + +class Note(Event): + def __init__(self, number, velocity, channel=0, track=0): + Event.__init__(self, Note) + self.number = number + self.velocity = velocity + self.channel = channel + self.track = track + + def dict(self): + return { + "type": "note", + "number": self.number, + "velocity": self.velocity, + "channel": self.channel + } + +def midi_events(): + if len(sys.argv) < 2: + raise ValueError("Il faut donner le nom du fichier: python3 "+sys.argv[0]+" fichier.mid") + return read_midi(sys.argv[1], float(getarg(2, 80))) + +_midi = None +_midi_active = {} +_midi_wait = 0 +_midi_tt = 0 +_midi_loop = True +def midi2(): + global _midi, _midi_active, _midi_wait, _midi_tt, _midi_loop + if _midi == None: + _midi = midi_events() + + if _midi_loop: + if _midi_wait == 0: + while True: + event = _midi[_midi_tt] + _midi_tt += 1 + if event.type == Note: + if event.velocity == 0: + if (event.number, event.track) in _midi_active: + _midi_active.pop((event.number, event.track)) + else: + _midi_active[(event.number, event.track)] = event.velocity + elif event.type == Wait: + _midi_wait = int(event.value*8000) + break + elif event.type == End: + _midi_loop = False + break + else: + _midi_wait -= 1 + + return [[idx[0], idx[1], _midi_active[idx]] for idx in _midi_active] + +""" +Lit le fichier MIDI donné en argument de la commande. +Retourne, pour chaque échantillon, la liste des notes sous la forme [piste, note, amplitude]. +""" +class Midi: + def __init__(self, sampling=16000): + self.sampling = sampling + + def __iter__(self): + self.active = {} + self.wait = 0 + self.tt = 0 + self.loop = True + self.events = midi_events() + return self + + def __next__(self): + if self.loop: + if self.wait == 0: + while True: + event = self.events[self.tt] + self.tt += 1 + if event.type == Note: + if event.velocity == 0: + if (event.track, event.number) in self.active: + self.active.pop((event.track, event.number)) + else: + sys.stderr.write("Piste="+str(event.track)+" Note="+str(event.number)+" Force="+str(event.velocity)+"\n") + self.active[(event.track, event.number)] = event.velocity + elif event.type == Wait: + self.wait = int(event.value*self.sampling/2) + break + elif event.type == End: + self.loop = False + break + else: + self.wait -= 1 + + return [[idx[0], idx[1], self.active[idx]] for idx in self.active] + raise StopIteration + +def read_midi(filename, tempo): + mid = mido.MidiFile(filename) + result = [] + tracks = [0 for i in mid.tracks] + tempo = [tempo for i in mid.tracks] + wait = [None for i in mid.tracks] + end = [False for i in mid.tracks] + t = 0 + loop = True + while loop: + loop = False + + # Find the first events + first = [] + firstt = None + for i, track in enumerate(mid.tracks): + if end[i]: + continue + if wait[i] == None: + wait[i] = track[tracks[i]].time/tempo[i]/4 + + if firstt == None or wait[i] < firstt: + firstt = wait[i] + first = [i] + elif wait[i] == firstt: + first.append(i) + + if firstt > 0: + result.append(Wait(firstt)) + + # Play the first events + for i, track in enumerate(mid.tracks): + if end[i]: + continue + loop = True + msg = track[tracks[i]] + if i in first: + # Play event + if msg.type == "set_tempo": + tempo[i] = 6e7/msg.tempo + elif msg.type == "note_on": + result.append(Note(msg.note-50, msg.velocity/100, msg.channel, i)) + elif msg.type == "end_of_track": + result.append(End()) + end[i] = True + + # Find next event + tracks[i] += 1 + wait[i] = None + else: + wait[i] -= firstt + + if not (False in end): + loop = False + + return result diff --git a/synthlib.py b/synthlib.py new file mode 100644 index 0000000..768b94c --- /dev/null +++ b/synthlib.py @@ -0,0 +1,246 @@ +#!/usr/bin/python3 + +import sys, os, mido + +DEFAULT_ENV = { + "sampling": 16000, + "signed": True, + "verbose": False, + "bits": 16, + "msbfirst": True, + "tempo": 130 +} + +FP = sys.stdout + +class SoundEncoder: + def __init__(self, env=DEFAULT_ENV, fp=sys.stdout, fp_log=sys.stderr): + self.env = env + self.fp = fp if fp.mode == "wb" else os.fdopen(fp.fileno(), 'wb') + self.fp_log = fp_log + + def write_sample(self, amplitude): + smax = 2**(self.env["bits"]-self.env["signed"])-1 + if self.env["signed"]: + v = int(amplitude*smax) + w = v + s = v < 0 + v = abs(v) + if s: + v = smax-v + v |= (s<<(self.env["bits"]-1)) + else: + v = int((amplitude*smax+smax)/2) + w = v + + if self.env["msbfirst"]: + i = 0 + while i < self.env["bits"]: + if self.fp.write(bytes([(v>>i)&255])) > 1: + self.fp_log.write(str(v)) + b = bin((v>>i)&255)[2:] + if self.env["verbose"]: + self.fp_log.write("0"*(8-len(b))+b) + i += 8 + else: + i = self.env["bits"] + while i > 0: + i -= 8 + self.fp.write(bytes([(v>>i)&255])) + b = bin((v>>i)&255)[2:] + if self.env["verbose"]: + self.fp_log.write("0"*(8-len(b))+b) + + if self.env["verbose"]: + self.fp_log.write(" "+"0"*(self.env["bits"]-len(bin(v))+2)+bin(v)[2:]+" "+str(v)+" "+str(w)+"\n") + +class Event: + def __init__(self, type): + self.type = type + +class Wait(Event): + def __init__(self, value): + Event.__init__(self, Wait) + self.value = value + + def dict(self): + return { + "type": "wait", + "number": self.value, + } + +class End(Event): + def __init__(self): + Event.__init__(self, End) + + def dict(self): + return { + "type": "end" + } + +class Note(Event): + def __init__(self, number, velocity, channel=0, track=0): + Event.__init__(self, Note) + self.number = number + self.velocity = velocity + self.channel = channel + self.track = track + + def dict(self): + return { + "type": "note", + "number": self.number, + "velocity": self.velocity, + "channel": self.channel + } + +def midi(): + return from_midi3(sys.argv[1]) + +def from_midi(filename, env=DEFAULT_ENV): + result = [] + t = 0 + tempo = env["tempo"] + mid = mido.MidiFile(filename) + for i, track in enumerate(mid.tracks): + for msg in track: + if msg.type == "set_tempo": + tempo = 6e7/msg.tempo + elif msg.type == "note_on": + if msg.time != 0: + result.append(Wait(msg.time/tempo/4)) + result.append(Note(msg.note-50, msg.velocity/100, msg.channel)) + elif msg.type == "end_of_track": + result.append(Wait(msg.time/tempo/4)) + result.append(End()) + return result + +def from_midi2(filename, env=DEFAULT_ENV): + mid = mido.MidiFile(filename) + result = [] + tracks = [0 for i in mid.tracks] + tempos = [env["tempo"] for i in mid.tracks] + wait = [None for i in mid.tracks] + end = [True for i in mid.tracks] + t = 0 + loop = True + while loop: + loop = False + for i, track in enumerate(mid.tracks): + if end[i]: + loop = True + continue + if wait[i] == None: + wait[i] = track[tracks[i]].time/tempos[i]/4 + #result.append(Wait(track[tracks[i]].time/tempo/4)) + if wait[i] <= 0: + msg = track[tracks[i]] + if t > 0: + result.append(Wait(t)) + t = 0 + if msg.type == "set_tempo": + tempos[i] = 6e7/msg.tempo + elif msg.type == "note_on": + result.append(Note(msg.note-50, msg.velocity/100, msg.channel, i)) + elif msg.type == "end_of_track": + result.append(End()) + end[i] = False + tracks[i] += 1 + if len(track) > tracks[i]: + wait[i] = track[tracks[i]].time/tempos[i]/4 + + else: + wait[i] -= 1 + + print(t) + t += 1 + print(result) + exit() + return result + +def from_midi3(filename, env=DEFAULT_ENV): + mid = mido.MidiFile(filename) + result = [] + tracks = [0 for i in mid.tracks] + tempo = [env["tempo"] for i in mid.tracks] + wait = [None for i in mid.tracks] + end = [False for i in mid.tracks] + t = 0 + loop = True + while loop: + loop = False + + # Find the first events + first = [] + firstt = None + for i, track in enumerate(mid.tracks): + if end[i]: + continue + if wait[i] == None: + wait[i] = track[tracks[i]].time/tempo[i]/4 + + if firstt == None or wait[i] < firstt: + firstt = wait[i] + first = [i] + elif wait[i] == firstt: + first.append(i) + + if firstt > 0: + result.append(Wait(firstt)) + + # Play the first events + for i, track in enumerate(mid.tracks): + if end[i]: + continue + loop = True + msg = track[tracks[i]] + if i in first: + # Play event + if msg.type == "set_tempo": + tempo[i] = 6e7/msg.tempo + elif msg.type == "note_on": + result.append(Note(msg.note-50, msg.velocity/100, msg.channel, i)) + elif msg.type == "end_of_track": + result.append(End()) + end[i] = True + + # Find next event + tracks[i] += 1 + wait[i] = None + else: + wait[i] -= firstt + + if not (False in end): + loop = False + + ### + """loop = False + for i, track in enumerate(mid.tracks): + if end[i]: + loop = True + continue + if wait[i] == None: + wait[i] = track[tracks[i]].time/tempo[i]/4 + #result.append(Wait(track[tracks[i]].time/tempo/4)) + if wait[i] <= 0: + msg = track[tracks[i]] + if t > 0: + result.append(Wait(t)) + t = 0 + if msg.type == "set_tempo": + tempo[i] = 6e7/msg.tempo + elif msg.type == "note_on": + result.append(Note(msg.note-50, msg.velocity/100, msg.channel, i)) + elif msg.type == "end_of_track": + result.append(End()) + end[i] = False + tracks[i] += 1 + if len(track) > tracks[i]: + wait[i] = track[tracks[i]].time/tempo[i]/4 + + else: + wait[i] -= 1 + + print(t) + t += 1""" + return result diff --git a/test2.mid b/test2.mid new file mode 100644 index 0000000..114cdea Binary files /dev/null and b/test2.mid differ diff --git a/tetris2.mid b/tetris2.mid new file mode 100644 index 0000000..835e157 Binary files /dev/null and b/tetris2.mid differ diff --git a/tuto/makefile b/tuto/makefile new file mode 100755 index 0000000..7fb0f6f --- /dev/null +++ b/tuto/makefile @@ -0,0 +1,7 @@ +all: tutorial.pdf + +tutorial.pdf: tutorial.html + wkhtmltopdf -s A4 --footer-center "[page] / [topage]" --footer-font-name "Libertinus Sans" --footer-font-size 11 --header-left "[subsection]" --header-right "[title] par Pascal Engélibert, 2025, CC BY-SA 4.0" --header-font-name "Libertinus Sans" --header-font-size 11 --margin-top 20mm --footer-spacing 7 --header-spacing 7 --margin-bottom 20mm tutorial.html tutorial.pdf + +tutorial.html: tutorial.md tutorial-head.html + pandoc tutorial.md -o tutorial.html --self-contained -M title="Tutoriel Python" --highlight-style pygments -H tutorial-head.html || exit 1 diff --git a/tuto/tutorial-head.html b/tuto/tutorial-head.html new file mode 100644 index 0000000..a2e4e43 --- /dev/null +++ b/tuto/tutorial-head.html @@ -0,0 +1,33 @@ + diff --git a/tuto/tutorial.html b/tuto/tutorial.html new file mode 100644 index 0000000..a7e6642 --- /dev/null +++ b/tuto/tutorial.html @@ -0,0 +1,427 @@ + + + + + + + Tutoriel Python + + + + +
+

Tutoriel Python

+
+

Tutoriel Python

+

Terminal

+

Ouvrir un terminal : CTRL+ALT+T ou le bouton noir en bas de +l’écran.

+

Commandes :

+ +

Raccourcis clavier :

+ +

Valeurs, variables, affichage

+

Afficher des valeurs avec print :

+
print(42)
+print(10, 20, 30)
+print("On peut écrire du texte.")
+print("Voici un nombre :", 123, ".")
+

Utiliser des variables :

+
annee = 2025
+print("Nous sommes en", annee)
+

Faire des calculs :

+
annee = 2025
+futur = annee + 10
+print("Dans 10 ans, nous serons en", futur)
+futur = futur + 5
+print("Dans 15 ans, nous serons en", futur)
+

Il y a d’autres opérateurs :

+ +

Faire des choix

+

Si (if) une valeur est vraie, alors je fais quelque chose :

+
if True:
+    print("True signifie vrai")
+    print("Donc ce code est toujours exécuté")
+if False:
+    print("False signifie faux")
+    print("Donc ce code n'est jamais exécuté")
+
+prix = 150
+if prix > 100:
+    print("C'est trop cher !")
+    prix = 100
+

Il y a d’autres comparateurs :

+ +

Sinon (else), je fais autre chose :

+
if 6*7 == 42:
+    print("bien !")
+else:
+    print("oups")
+

On peut aussi enchaîner plusieurs else if :

+
if nombre > 0:
+    print("le nombre est strictement positif")
+elif nombre < 0:
+    print("le nombre est strictement négatif")
+else:
+    print("le nombre est nul")
+

Boucle for

+

On peut exécuter un code un nombre donné de fois :

+
print("Je vais compter de 0 à 9 !")
+for i in range(10):
+    print(i)
+
+print("Je vais compter de 1 à 10 !")
+for i in range(1, 11):
+    print(i)
+

Listes

+

Une liste est plusieurs valeurs entre crochets.

+

On peut ajouter un élément à la fin avec append, et +coller deux listes avec +.

+
fruits = []
+print("C'est vide :", fruits)
+fruits.append("pomme")
+fruits.append("kiwi")
+fruits = fruits + ["abricot", "orange"]
+

La boucle for exécute un code pour chaque élément de la +liste, dans l’ordre.

+
for fruit in fruits:
+    print("Ceci est un fruit :", fruit)
+

On peut lire un élément donné du tableau (la numérotation commence à +0) :

+
file = ["Toto", "Titi", "Tata", "Tutu"]
+print("La deuxième personne dans la file est", file[1])
+

On peut changer la valeur d’un élément de la liste :

+
file[2] = "Anne Onyme"
+

On peut aussi faire des listes de listes :

+
stock = [
+    ["patates", 52],
+    ["poireaux", 20],
+    ["carottes", 40]
+]
+for element in stock:
+    print("Nombre de", element[0], ":", element[1])
+

Pour avoir à la fois l’indice (le numéro) de l’élément, et l’élément +:

+
planetes = ["Mercure", "Vénus", "Terre", "Mars"]
+for i in range(len(planetes)):
+    print("La planète numéro", i+1, "se nomme", planetes[i])
+

On peut retirer un élément avec pop.

+
planetes = ["Lune", "Jupiter", "Saturne"]
+# Oups, la Lune n'est pas une planète.
+print("Je retire", planetes.pop(0), "de la liste.")
+

On peut agrandir une liste en répétant un élément un nombre donné de +fois :

+
zeros = [0] * 1000
+print("Mille zéros !", zeros)
+

Fonctions mathématiques

+ +

Pour avoir des fonctions plus avancées, il faut importer le module +math au début du fichier :

+
import math
+ +

Aléatoire

+

Il faut importer le module random au début du fichier +:

+
import random
+ +

Fonctions anonymes

+ + diff --git a/tuto/tutorial.md b/tuto/tutorial.md new file mode 100644 index 0000000..f275208 --- /dev/null +++ b/tuto/tutorial.md @@ -0,0 +1,232 @@ +# Tutoriel Python + +## Terminal + +Ouvrir un terminal : CTRL+ALT+T ou le bouton noir en bas de l'écran. + +Commandes : + +* `ls` : **l**i**s**ter les fichiers et dossiers +* `cd mondossier` : **c**hanger de **d**ossier +* `cd ..` : aller dans le dossier parent +* `mkdir mondossier` : créer le dossier +* `python3 monfichier.py` : lancer le fichier Python +* `mv ancien nouveau` : déplacer ou renommer un fichier ou dossier +* `cp ancien nouveau` : copier un fichier + +Raccourcis clavier : + +* `CTRL+C` : arrêter la commande +* `Tab` : compléter automatiquement le nom de fichier +* flèche haut/bas : historique des commandes + +## Valeurs, variables, affichage + +Afficher des valeurs avec `print` : + +```python +print(42) +print(10, 20, 30) +print("On peut écrire du texte.") +print("Voici un nombre :", 123, ".") +``` + +Utiliser des variables : + +```python +annee = 2025 +print("Nous sommes en", annee) +``` + +Faire des calculs : + +```python +annee = 2025 +futur = annee + 10 +print("Dans 10 ans, nous serons en", futur) +futur = futur + 5 +print("Dans 15 ans, nous serons en", futur) +``` + +Il y a d'autres opérateurs : + +* `+` : addition +* `-` : soustraction +* `*` : multiplication +* `/` : division +* `%` : modulo (reste de la division) +* `**` : puissance + +## Faire des choix + +Si (if) une valeur est vraie, alors je fais quelque chose : + +```python +if True: + print("True signifie vrai") + print("Donc ce code est toujours exécuté") +if False: + print("False signifie faux") + print("Donc ce code n'est jamais exécuté") + +prix = 150 +if prix > 100: + print("C'est trop cher !") + prix = 100 +``` + +Il y a d'autres comparateurs : + +* `==` : égal +* `!=` : différent +* `<` : inférieur +* `>` : supérieur +* `<=` : inférieur ou égal +* `>=` : supérieur ou égal + +Sinon (else), je fais autre chose : + +```python +if 6*7 == 42: + print("bien !") +else: + print("oups") +``` + +On peut aussi enchaîner plusieurs else if : + +```python +if nombre > 0: + print("le nombre est strictement positif") +elif nombre < 0: + print("le nombre est strictement négatif") +else: + print("le nombre est nul") +``` + +## Boucle for + +On peut exécuter un code un nombre donné de fois : + +```python +print("Je vais compter de 0 à 9 !") +for i in range(10): + print(i) + +print("Je vais compter de 1 à 10 !") +for i in range(1, 11): + print(i) +``` + +## Listes + +Une liste est plusieurs valeurs entre crochets. + +On peut ajouter un élément à la fin avec `append`, et coller deux listes avec `+`. + +```python +fruits = [] +print("C'est vide :", fruits) +fruits.append("pomme") +fruits.append("kiwi") +fruits = fruits + ["abricot", "orange"] +``` + +La boucle `for` exécute un code pour chaque élément de la liste, dans l'ordre. + +```python +for fruit in fruits: + print("Ceci est un fruit :", fruit) +``` + +On peut lire un élément donné du tableau (la numérotation commence à 0) : + +```python +file = ["Toto", "Titi", "Tata", "Tutu"] +print("La deuxième personne dans la file est", file[1]) +``` + +On peut changer la valeur d'un élément de la liste : + +```python +file[2] = "Anne Onyme" +``` + +On peut aussi faire des listes de listes : + +```python +stock = [ + ["patates", 52], + ["poireaux", 20], + ["carottes", 40] +] +for element in stock: + print("Nombre de", element[0], ":", element[1]) +``` + +Pour avoir à la fois l'indice (le numéro) de l'élément, et l'élément : + +```python +planetes = ["Mercure", "Vénus", "Terre", "Mars"] +for i in range(len(planetes)): + print("La planète numéro", i+1, "se nomme", planetes[i]) +``` + +On peut retirer un élément avec `pop`. + +```python +planetes = ["Lune", "Jupiter", "Saturne"] +# Oups, la Lune n'est pas une planète. +print("Je retire", planetes.pop(0), "de la liste.") +``` + +On peut agrandir une liste en répétant un élément un nombre donné de fois : + +```python +zeros = [0] * 1000 +print("Mille zéros !", zeros) +``` + +## Fonctions mathématiques + +* `max(a, b)` : donne le plus grand nombre entre a et b +* `min(a, b)` : donne le plus petit nombre entre a et b +* `max(liste)` : donne la valeur maximum de la liste +* `min(liste)` : donne la valeur minimum de la liste +* `sum(liste)` : donne la somme de tous les éléments de la liste +* `int(nombre)` : partie entière (enlève les chiffres après la virgule) +* `round(nombre)` : arrondit au plus proche +* `abs(nombre)` : valeur absolue (distance à zéro) + +Pour avoir des fonctions plus avancées, il faut importer le module `math` au début du fichier : + +```python +import math +``` + +* `math.pi` : 3.1415926... +* `math.sin(x)` : sinus +* `math.cos(x)` : cosinus +* `math.floor(x)` : arrondit à l'inférieur +* `math.ceil(x)` : arrondit au supérieur + +## Aléatoire + +Il faut importer le module `random` au début du fichier : + +```python +import random +``` + +* `random.random()` : nombre (à virgule) aléatoire entre 0 et 1 +* `random.randint(100, 200)` : nombre (entier) aléatoire entre 100 (inclus) et 200 (inclus) +* `random.choice(liste)` : choisit un élément au hasard dans la liste + +## Fonctions anonymes + + + + + + + diff --git a/tuto/tutorial.pdf b/tuto/tutorial.pdf new file mode 100644 index 0000000..ec61dc9 Binary files /dev/null and b/tuto/tutorial.pdf differ diff --git a/zelda.mid b/zelda.mid new file mode 100644 index 0000000..8dfb8e2 Binary files /dev/null and b/zelda.mid differ