Initial commit

This commit is contained in:
Pascal Engélibert 2025-02-27 00:41:31 +01:00
commit 778fe31c38
35 changed files with 1849 additions and 0 deletions

Binary file not shown.

11
stage/level1.py Normal file
View file

@ -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)

12
stage/level2.py Normal file
View file

@ -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))

25
stage/level3.py Normal file
View file

@ -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))

25
stage/level4.py Normal file
View file

@ -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)

27
stage/level5.py Normal file
View file

@ -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

48
stage/level6.py Normal file
View file

@ -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

237
stage/synth.py Normal file
View file

@ -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