Source code for pyaudiosynth.synthesizer
from math import pi
import matplotlib.pyplot as plt
import numpy as np
import sounddevice as sd
from librosa import note_to_hz
from .note import Note
from .wave import Wave
[docs]
class Synthesizer:
"""
Main class for synthesizing notes
:param default_nlen: The default note length in milliseconds (default 500)
:type default_nlen: int
:param default_wave: The default waveform to use (default Sine)
:type default_wave: Wave
:param show_vis: If we should show the waveform visualisation (default False)
:type show_vis: bool
"""
def __init__(self, default_nlen: int = 500, default_wave: Wave = Wave.Sine, show_vis: bool = False) -> None:
self.samplerate = 44100
self.default_nlen = default_nlen
self.default_wave = default_wave
self.show_vis = show_vis
if show_vis:
plt.ion()
self.fig, self.ax = plt.subplots(figsize=(10, 4))
[docs]
def play_frq(self, frq: float, ms: float, wave: Wave) -> None:
"""
Plays a certain frequency
:param frq: The frequency to play
:type frq: float
:param ms: Milisecconds to play the note for
:type ms: float
:param wave: The waveform to play
:type wave: Wave
"""
nsamples = self.samplerate * (ms / 1000)
phase_values = np.arange(nsamples) * (2 * pi * frq / self.samplerate)
match wave:
case Wave.Sine:
audio = np.sin(phase_values)
case Wave.Square:
audio = np.sign(np.sin(phase_values))
case Wave.Saw:
audio = 2 * (phase_values / (2 * pi) % 1) - 1
case Wave.Triangle:
audio = 2 * np.abs(2 * (phase_values / (2 * pi) % 1) - 1) - 1
if self.show_vis:
self.ax.clear()
samples_to_show = int(0.01 * self.samplerate)
time = np.arange(samples_to_show) / self.samplerate
self.ax.plot(time, audio[:samples_to_show])
self.ax.set_xlabel('Time (s)')
self.ax.set_ylabel('Amplitude')
self.ax.set_title(f'{wave.name.capitalize()} wave - {frq:.1f} Hz')
self.ax.grid(True)
self.ax.set_ylim(-1.5, 1.5)
plt.draw()
plt.pause(0.001)
sd.play(audio.astype(np.float32), self.samplerate)
sd.wait()
[docs]
def play_note(self, note: Note|list[Note]) -> None:
"""
Plays note(s)
:param note: The note, or list of notes to play
:type note: Note|list[Note]
"""
self.play_frq(
float(note_to_hz(note.name)),
note.ms if note.ms is not None else self.default_nlen,
note.wave if note.wave is not None else self.default_wave
) if isinstance(note, Note) else tuple(self.play_note(nt) for nt in note) # tuple forces execution
[docs]
def play_notes_ww(self, note: Note|list[Note], wave: Wave) -> None:
"""
Plays note(s), ignoring the notes waveform instead using the provided waveform
:param note: The note, or list of notes to play
:type note: Note|list[Note]
:param wave: The waveform which we should play
:type wave: Wave
"""
self.play_note(
list(Note(nt.name, nt.ms, wave) for nt in note)
if isinstance(note, list)
else Note(note.name, note.ms, wave)
)
[docs]
def play_sine(self, note: Note|list[Note]) -> None:
"""
Plays note(s), ignoring the notes waveform instead using a Sine wave
:param note: The note, or list of notes to play
:type note: Note|list[Note]
"""
self.play_notes_ww(note, Wave.Sine)
[docs]
def play_square(self, note: Note|list[Note]) -> None:
"""
Plays note(s), ignoring the notes waveform instead using a Square wave
:param note: The note, or list of notes to play
:type note: Note|list[Note]
"""
self.play_notes_ww(note, Wave.Square)
[docs]
def play_saw(self, note: Note|list[Note]) -> None:
"""
Plays note(s), ignoring the notes waveform instead using a Sawtooth wave
:param note: The note, or list of notes to play
:type note: Note|list[Note]
"""
self.play_notes_ww(note, Wave.Saw)
[docs]
def play_triangle(self, note: Note|list[Note]) -> None:
"""
Plays note(s), ignoring the notes waveform instead using a Triangle wave
:param note: The note, or list of notes to play
:type note: Note|list[Note]
"""
self.play_notes_ww(note, Wave.Triangle)