171 wiersze
6.0 KiB
Python
171 wiersze
6.0 KiB
Python
# test generowania dzwieku stereo jako oscyloskop-rendering X/Y,
|
|
# dynamiczna zmiany danych sampli sterowana z klawiatury:
|
|
# - zmiany pozycji obiektu X/Y kur sorami
|
|
# - zmiana kształtu obektu spacją
|
|
|
|
import numpy as np
|
|
import sounddevice as sd
|
|
import threading
|
|
import time
|
|
from pynput import keyboard
|
|
|
|
# Param etry
|
|
sample_rate = 44100 # częstotliwość próbkowania (Hz)
|
|
sample_duration = 1 # czas trwania próbki (sekundy)
|
|
duration = 30 # czas odtwarzania dźwięku (sekundy)
|
|
|
|
frequency = 440 # początkowa częstotliwość dźwięku (Hz)
|
|
scale = 0.5 # skala poczatkowa obrazu
|
|
|
|
# zródłowe sample do aktualnego kształtu
|
|
l_org_samples = np.empty(0)
|
|
r_org_samples = np.empty(0)
|
|
|
|
# wynikowe sample , po przekształeceniach, z których będzie wygenerowany dźwięk
|
|
l_out_samples = np.empty(0)
|
|
r_out_samples = np.empty(0)
|
|
|
|
# Mechanizm synchronizacji i konczenie wątku
|
|
lock = threading.Lock()
|
|
stop_thread = False
|
|
|
|
# zmienne z aktualym przesunięciem X/Y obiektu generowanego z sampli
|
|
dx = 0.0
|
|
dy = 0.0
|
|
|
|
shape = False # False = koło , True = kwadrat
|
|
|
|
# definicja funkcji trapezowej - do generowania kształtu kwadratu na ekranie X/Y
|
|
def trapezoid(angle):
|
|
"""
|
|
Funkcja generuje sygnał w postaci trapezowego przebiegu na podstawie kąta.
|
|
Zakłada 4 segmenty:
|
|
- pierwszy: liniowy wzrost
|
|
- drugi: stała funkcja
|
|
- trzeci: liniowy spadek
|
|
- czwarty: stała funkcja
|
|
"""
|
|
# Dopasowanie kąta do zakresu 0..2*pi (obsługuje kąty większe niż 2*pi)
|
|
angle = angle % (2 * np.pi)
|
|
|
|
# Sygnał o wartościach -1 do 1
|
|
if 0 <= angle < np.pi / 2: # Pierwszy segment: rosnąca funkcja liniowa
|
|
return 2 * (angle / (np.pi / 2)) - 1 # Liniowy wzrost od -1 do 1
|
|
elif np.pi / 2 <= angle < np.pi: # Drugi segment: stała funkcja
|
|
return 1 # Funkcja stała (maksimum)
|
|
elif np.pi <= angle < 3 * np.pi / 2: # Trzeci segment: malejąca funkcja liniowa
|
|
return -2 * (angle - np.pi) / (np.pi / 2) + 1 # Liniowy spadek od 1 do -1
|
|
elif 3 * np.pi / 2 <= angle < 2 * np.pi: # Czwarty segment: stała funkcja
|
|
return -1 # Funkcja stała (minimum)
|
|
return 0 # Domyślny przypadek
|
|
|
|
# kształt = koło
|
|
def gen_circle():
|
|
global l_org_samples, r_org_samples
|
|
t = np.arange(0, sample_duration, 1/sample_rate) # czas
|
|
l_org_samples = np. sin(2 * np.pi * frequency * t) * scale # lewy kanał
|
|
r_org_samples = np.cos(2 * np.pi * frequency * t) * scale # prawy kanał
|
|
|
|
# kształt = kwadrat ( na podstawie f.kwadratowej, ale to renderuje 4 punkty a nie ślad kwadratu)
|
|
def gen_square4():
|
|
global l_org_samples, r_org_samples
|
|
t = np.arange(0, sample_duration, 1/sample_rate) # czas
|
|
l_org_samples = np.sign(np.sin(2 * np.pi * frequency * t)) * scale # lewy kanał
|
|
r_org_samples = np.sign(np.cos(2 * np.pi * frequency * t)) * scale # prawy kanał
|
|
|
|
# ksztalt = kwadrat ale rysowany funkcją trapezową
|
|
def gen_square():
|
|
global l_org_samples, r_org_samples
|
|
t = np.arange(0, sample_duration, 1/sample_rate) # czas
|
|
angles = 2 * np.pi * t # Kąt w pełnym okresie 0 do 2*pi
|
|
l_org_samples = np.array([trapezoid(angle * frequency) for angle in angles]) * scale # lewy kanał , faza = 0
|
|
r_org_samples = np.array([trapezoid(angle * frequency + np.pi/2) for angle in angles]) * scale # prawy kanał , faza = pi/2
|
|
|
|
def set_shape(shape):
|
|
if shape:
|
|
gen_square()
|
|
else:
|
|
gen_circle()
|
|
|
|
# generowanie zmodyfikowanych sampli dla obrazu przesunietego o dx,dy
|
|
def gen_samples(dx, dy):
|
|
global l_out_samples, r_out_samples
|
|
|
|
with lock:
|
|
l_out_samples = l_org_samples + dx
|
|
r_out_samples = r_org_samples + dy
|
|
|
|
# Funkcja do generowania dźwięku na żywo
|
|
def live_audio_callback(outdata, frames, time, status):
|
|
# Funkcja zwrotna, która będzie wywoływana przez sounddevice do przetwarzania próbek
|
|
if status:
|
|
print(status)
|
|
|
|
global l_out_samples, r_out_samples
|
|
|
|
with lock:
|
|
outdata[:, 0] = l_out_samples[:frames]
|
|
outdata[:, 1] = r_out_samples[:frames]
|
|
l_out_samples = np.roll(l_out_samples, -frames)
|
|
r_out_samples = np.roll(r_out_samples, -frames)
|
|
|
|
# Ustawiamy parametry i uruchamiamy odtwarzanie dźwięku
|
|
def play_audio():
|
|
global stop_thread, current_position
|
|
print("Odtwarzanie dźwięku...\n")
|
|
while not stop_thread:
|
|
with sd.OutputStream(channels=2, samplerate=sample_rate, callback=live_audio_callback):
|
|
sd.sleep(10000) # Odtwarzanie przez określony czas (w ms)
|
|
|
|
|
|
# Funkcja wywoływana, gdy naciśniemy klawisz
|
|
def on_press(key):
|
|
global dx,dy,shape
|
|
try:
|
|
if key == keyboard.Key.up:
|
|
dy = 1.0 if dy >= 1.0 else dy + 0.1
|
|
print(f"X: {dx}, Y: {dy}")
|
|
gen_samples(dx, dy)
|
|
|
|
elif key == keyboard.Key.down:
|
|
dy = -1.0 if dy <= -1.0 else dy - 0.1
|
|
print(f"X: {dx}, Y: {dy}")
|
|
gen_samples(dx, dy)
|
|
|
|
elif key == keyboard.Key.left:
|
|
dx = -1.0 if dx <= -1.0 else dx - 0.1
|
|
print(f"X: {dx}, Y: {dy}")
|
|
gen_samples(dx, dy)
|
|
|
|
elif key == keyboard.Key.right:
|
|
dx = 1.0 if dx >= 1.0 else dx + 0.1
|
|
print(f"X: {dx}, Y: {dy}")
|
|
gen_samples(dx, dy)
|
|
|
|
elif key == keyboard.Key.space:
|
|
shape = not shape
|
|
print("Kształt zmieniony")
|
|
set_shape(shape)
|
|
gen_samples(dx, dy)
|
|
|
|
elif key == keyboard.Key.esc:
|
|
print("Zakończenie programu!")
|
|
return False # Zatrzymanie nasłuchiwania
|
|
except AttributeError:
|
|
pass
|
|
|
|
# Odtwarzanie dźwięku w osobnym wątku
|
|
set_shape(shape)
|
|
gen_samples(dx, dy)
|
|
thread = threading.Thread(target=play_audio)
|
|
thread.start()
|
|
|
|
print("klawisze strzałek: zmiana pozycji X/Y\nklawisz spacji: zmiana kształtu\nklawisz ESC - wyjście")
|
|
print(f"X: {dx}, Y: {dy}")
|
|
|
|
# Listener do nasłuchiwania klawiszy
|
|
with keyboard.Listener(on_press=on_press) as listener:
|
|
listener.join()
|
|
|
|
stop_thread = True
|