Source code for jdxi_editor.ui.image.waveform
"""
waveform_icons
This module provides functions to generate PNG images representing different waveform icons
using the Python Imaging Library (PIL). Each function returns address base64-encoded string of
the generated image.
Functions:
generate_waveform_icon(icon_type, foreground_color, icon_scale): Generates address
- triangle: Generates address triangle waveform icon.
- upsaw: Generates an upward sawtooth waveform icon.
- square: Generates address square waveform icon.
- sine: Generates address sine waveform icon.
- noise: Generates address noise waveform icon.
- spsaw: Generates address special sawtooth waveform icon.
- pcm: Generates address PCM waveform icon.
- adsr: Generates an ADSR envelope waveform icon.
"""
import base64
import math
from io import BytesIO
from PIL import Image, ImageColor, ImageDraw
from PySide6.QtGui import QPixmap
from jdxi_editor.midi.data.digital.oscillator import WaveForm
from jdxi_editor.ui.image.utils import base64_to_pixmap
[docs]
def generate_waveform_icon(
waveform: str, foreground_color: str, icon_scale: float
) -> str:
"""
Generate address waveform icon as address base64-encoded PNG image
:param waveform: str
:param foreground_color: str
:param icon_scale: float
:return: icon
:rtype: str
"""
x = int(17 * icon_scale)
y = int(9 * icon_scale)
th = int(icon_scale + 0.49)
im = Image.new("RGBA", (x, y), (255, 255, 255, 0))
draw = ImageDraw.Draw(im)
color = ImageColor.getrgb(foreground_color)
half_y = y * 0.5
quarter_x = x * 0.25
three_quarters_x = x * 0.75
if waveform == WaveForm.TRIANGLE:
draw.line(
[(0, half_y), (quarter_x, 0), (three_quarters_x, y - 1), (x, half_y)],
fill=color,
width=th,
)
elif waveform == WaveForm.UPSAW:
draw.line(
[(0, y - 1), (x * 0.5, 0), (x * 0.5, y - 1), (x - 1, 0)],
fill=color,
width=th,
)
elif waveform == WaveForm.SQUARE:
draw.line(
[
(th * 0.5, y - 1),
(th * 0.5, 0),
(x * 0.5, 0),
(x * 0.5, y - 1),
(x - th * 0.5, y - 1),
(x - th * 0.5, 0),
],
fill=color,
width=th,
)
elif waveform == WaveForm.SINE:
# Define the number of points for smoothness
num_points = 60
sine_wave = [
(
i * x / (num_points - 1),
half_y + (math.sin(i * 2 * math.pi / (num_points - 1)) * half_y * 0.8),
)
for i in range(num_points)
]
draw.line(sine_wave, fill=color, width=th)
elif waveform == WaveForm.LPF_FILTER:
"""
Low-pass filter icon:
Full amplitude on the left, progressively attenuated to the right,
visually representing a low-pass filter's frequency response.
"""
num_points = 80
points = []
for i in range(num_points):
t = i / (num_points - 1) # 0 → 1 across X
x_pos = t * (x - 1)
# Sigmoid-style amplitude drop for LPF
# Left: full height, Right: approaches 0
# Shifted so the drop starts ~30% from left
y_pos = half_y + half_y * (1 - 1 / (1 + math.exp(-12 * (t - 0.3))))
# Flip vertically so 0 is bottom of canvas
y_pos = y - y_pos
points.append((x_pos, y_pos))
draw.line(points, fill=color, width=th)
elif waveform == WaveForm.HPF_FILTER:
"""
High-pass filter icon:
Low amplitude on the left, progressively increasing to full amplitude on the right,
visually representing a high-pass filter's frequency response.
"""
num_points = 80
points = []
for i in range(num_points):
t = i / (num_points - 1) # 0 → 1 across X
x_pos = t * (x - 1)
# Sigmoid-style amplitude rise for HPF
# Left: approaches 0, Right: full height
# Shifted so the rise starts ~30% from left
y_pos = half_y + half_y * (1 / (1 + math.exp(-12 * (t - 0.3))))
# Flip vertically so 0 is bottom of canvas
y_pos = y - y_pos
points.append((x_pos, y_pos))
draw.line(points, fill=color, width=th)
elif waveform == WaveForm.BPF_FILTER:
"""
Band-pass filter icon:
Low frequencies attenuated, middle frequencies pass, high frequencies attenuated.
Smooth bump in the middle representing the passband.
"""
num_points = 80
points = []
for i in range(num_points):
t = i / (num_points - 1) # 0 → 1 across X
x_pos = t * (x - 1)
# High-pass sigmoid: rises around 0.2
hp = 1 / (1 + math.exp(-12 * (t - 0.2)))
# Low-pass sigmoid: falls around 0.8
lp = 1 / (1 + math.exp(-12 * (0.8 - t)))
# Multiply for band-pass effect
amplitude = hp * lp
y_pos = half_y + half_y * amplitude
# Flip vertically so 0 is bottom
y_pos = y - y_pos
points.append((x_pos, y_pos))
draw.line(points, fill=color, width=th)
elif waveform == WaveForm.BYPASS_FILTER:
"""
Bypass filter icon:
A straight horizontal line representing no filtering - signal passes through unchanged.
The abrupt, flat line visually represents bypass mode.
"""
# Draw a straight horizontal line at the middle (representing full signal, no attenuation)
draw.line(
[(0, half_y), (x - 1, half_y)],
fill=color,
width=th,
)
elif waveform == WaveForm.FILTER_SINE:
"""
Low-pass filter icon:
A waveform whose amplitude decreases from left to right,
visually representing high-frequency attenuation.
"""
num_points = 60
points = []
for i in range(num_points):
t = i / (num_points - 1) # 0 → 1 across X
x_pos = t * (x - 1)
# Amplitude rolls off toward the right (LPF effect)
amplitude = half_y * (1.0 - 0.75 * t)
# Slightly flattened sine to feel more "filtered"
y_pos = half_y + math.sin(t * 2.5 * math.pi) * amplitude * 0.9
points.append((x_pos, y_pos))
draw.line(points, fill=color, width=th)
elif waveform == WaveForm.NOISE:
import random
points = [
(th * 0.5 + x * 0.0588 * i, y * (0.5 + random.uniform(-0.4, 0.4)))
for i in range(16)
]
draw.line(points, fill=color, width=th)
elif waveform == WaveForm.SPSAW:
draw.line(
[(0, half_y), (y * 0.5, 0), (y * 0.5, y - 1), (x - 1, half_y)],
fill=color,
width=th,
)
elif waveform == WaveForm.PCM:
for i in range(12):
draw.line(
[
(x * (0.1 * i), y),
(x * (0.1 * i), y - (y * (0.4 + 0.2 * (-1) ** i))),
],
fill=color,
width=th,
)
elif waveform == WaveForm.PWSQU:
draw.line([(th * 0.5, y - 1), (th * 0.5, 0)], fill=color, width=th)
draw.line(
[(0, th * 0.5), (x * 0.68 - th * 0.5, th * 0.5)], fill=color, width=th
)
draw.line([(x * 0.68, 0), (x * 0.68, y - 1)], fill=color, width=th)
draw.line(
[(x * 0.68, y - th * 0.5), (x - 1, y - th * 0.5)], fill=color, width=th
)
draw.line([(x - th * 0.5, y - 1), (x - th * 0.5, 0)], fill=color, width=th)
elif waveform == WaveForm.ADSR:
# rgb = tuple(int(foreground_color[i : i + 2], 16) for i in (1, 3, 5))
width = int(17 * icon_scale)
height = int(9 * icon_scale)
# th = int(icon_scale + 0.49)
line_color = foreground_color
im = Image.new("RGBA", (width, height), (0, 0, 0, 0))
draw = ImageDraw.Draw(im)
# Define the ADSR shape
points = [
(0.2 * width, height * 1), # Start
(0.3 * width, height * 0), # Attack
(0.5 * width, height * 0.4), # Decay
(0.7 * width, height * 0.4), # Sustain
(0.9 * width, height * 1), # Release
]
# Draw the ADSR shape
draw.line(points, fill=line_color, width=3)
buffer = BytesIO()
im.save(buffer, format="PNG")
return base64.b64encode(buffer.getvalue()).decode("utf-8")
[docs]
def generate_icon_from_waveform(icon_name: str) -> QPixmap:
"""Generate icon from waveform type"""
# Lazy import to avoid circular dependency
from jdxi_editor.ui.style import JDXiUIStyle
icon_base64 = generate_waveform_icon(icon_name, JDXiUIStyle.WHITE, 1.0)
pixmap = base64_to_pixmap(icon_base64)
return pixmap