-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcedargrove_waveviz.py
260 lines (226 loc) · 8.96 KB
/
cedargrove_waveviz.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# SPDX-FileCopyrightText: Copyright (c) 2024 JG for Cedar Grove Maker Studios
# SPDX-License-Identifier: MIT
"""
`cedargrove_waveviz`
===============================================================================
A CircuitPython class to create a positionable ``displayio.TileGrid`` object
from a ``synthio.waveform`` wave table or ``synthio.Envelope`` object.
The class inherits the properties of a ``TileGrid`` object including
``bitmap``, ``pixel_shader``, ``width``, ``height``, ``x``, ``y``, and
provides the bitmap properties of ``width``, ``height``.
https://github.com/CedarGroveStudios/CircuitPython_WaveViz
https://docs.circuitpython.org/en/latest/shared-bindings/displayio/#displayio.TileGrid
* Author(s): JG for Cedar Grove Maker Studios
Implementation Notes
--------------------
**Software and Dependencies:**
* Adafruit CircuitPython firmware for the supported boards:
https://circuitpython.org/downloads
"""
from array import array
import displayio
import synthio
import bitmaptools
# pylint: disable=too-few-public-methods
# pylint: disable=too-many-instance-attributes
class WaveViz(displayio.TileGrid):
"""
The WaveViz class creates a positionable ``displayio.TileGrid`` object
from a ``synthio.ReadableBuffer`` wave table or ``synthio.Envelope``
object. The class inherits the properties of a ``TileGrid`` object of
``bitmap``, ``pixel_shader``, ``width``, ``height``, ``x``, ``y``.
:param union(synthio.ReadableBuffer, synthio.Envelope) wave_table: The
synthio waveform or envelope object. Wave table of type 'h' (signed
16-bit) or envelope object of type `synthio.Envelope`. No default.
:param int x: The tile grid's x-axis coordinate value. No default.
:param int y: The tile grid's y-axis coordinate value. No default.
:param int width: The tile grid's width in pixels. No default.
:param int height: The tile grid's height in pixels. No default.
:param integer plot_color: The waveform trace color. Defaults to 0x00FF00 (green).
:param integer grid_color: The perimeter grid color. Defaults to 0x808080 (gray).
:param integer back_color: The grid background color. Defaults to None (transparent).
:param bool auto_scale: Automatically adjust resultant plot to the wave table's
full-scale value. Defaults to True (auto scale enabled).
"""
# pylint: disable=too-many-arguments
def __init__(
self,
wave_table,
x,
y,
width,
height,
plot_color=0x00FF00,
grid_color=0x808080,
back_color=None,
auto_scale=True,
):
"""Instantiate the tile generator class."""
self._wave_table = wave_table
self._x = x
self._y = y
self._width = width
self._height = height
self._y_offset = self._height // 2
self._auto_scale = auto_scale
self._max_sample_value = 32767 # Maximum signed 16-bit value
self._scale_y = 0 # Define for later use
if isinstance(self._wave_table, synthio.Envelope):
self._envelope_plot = True
else:
self._envelope_plot = False
self._palette = displayio.Palette(3)
self._palette[1] = plot_color
self._palette[2] = grid_color
if back_color is None:
self._palette[0] = 0x000000
self._palette.make_transparent(0)
else:
self._palette[0] = back_color
# Instantiate the target bitmap
self._bmp = displayio.Bitmap(self._width, self._height, len(self._palette))
self._bmp.fill(0)
# Plot grid and wave table
self._update_plot()
# Bitmap becomes a displayio.TileGrid object
super().__init__(self._bmp, pixel_shader=self._palette, x=self._x, y=self._y)
@property
def wave_table(self):
"""The synthio waveform array object."""
return self._wave_table
@wave_table.setter
def wave_table(self, new_wave_table):
self._wave_table = new_wave_table
if isinstance(self._wave_table, synthio.Envelope):
self._envelope_plot = True
else:
self._envelope_plot = False
self._update_plot()
@property
def width(self):
"""The width of the plotted image in pixels."""
return self._width
@property
def height(self):
"""The height of the plotted image in pixels."""
return self._height
@property
def auto_scale(self):
"""Automatically adjust resultant plot to the wave table's
full-scale value."""
return self._auto_scale
@auto_scale.setter
def auto_scale(self, new_auto_scale):
self._auto_scale = new_auto_scale
self._update_plot()
@property
def max_result(self):
"""The full-scale value of the plotted image."""
return self._max_sample_value
def _update_plot(self):
"""Clears the bitmap and plots the grid and waveform or envelope."""
# Clear the target bitmap
self._bmp.fill(0)
# Plot grid and wave table
self._plot_grid() # Plot the grid
if self._envelope_plot:
self._plot_envelope()
else:
self._plot_wave() # Plot the wave table
def _plot_envelope(self):
"""Plot the wave_table as a bitmap representing an ADSR envelope
object. Y-axis is set at 0.0 to 1.0 and will not automatically
scale. Sustain duration is plotted as an arbitrary value based
on the attack and release time values."""
# Get the five envelope values from the wave table
a_time = self._wave_table[0] # Attack time
a_level = self._wave_table[3] # Attack level
d_time = self._wave_table[1] # Decay time
s_level = self._wave_table[4] # Sustain level
r_time = self._wave_table[2] # Release time
x_points = array("h", [])
y_points = array("h", [])
# Plot envelope polygon
if s_level != 0:
# Full ADSR envelope
s_time = 0.3 * (a_time + r_time) # relative/arbitrary, not actual
time_points = [
0,
a_time,
a_time + d_time,
a_time + d_time + s_time,
a_time + d_time + s_time + r_time,
]
level_points = [0, a_level, s_level, s_level, 0]
env_duration = a_time + d_time + s_time + r_time + 0.0001
else:
# AR phases only (plucked or struck instrument)
time_points = [0, a_time, a_time + r_time]
level_points = [0, a_level, 0]
env_duration = a_time + r_time + 0.0001
# Scale the lists to fit the plot window size and location in pixels
for time in time_points:
x_points.append(int((self._width / env_duration) * time))
for level in level_points:
y_points.append(-int(self._height * level) + self._height)
# Draw the envelope polygon
bitmaptools.draw_polygon(
self._bmp,
x_points,
y_points,
1,
False,
)
def _plot_wave(self):
"""Plot the wave_table as a bitmap. Extract samples from the wave
table to fill the bitmap object's x-axis. Y-axis scale factor is
determined from the extracted sample values."""
samples = len(self._wave_table) # Samples in wave table
# Create and fill the polygon arrays
x_points = array("h", [])
y_points = array("h", [])
for x in range(self._width):
x_points.append(x)
table_idx = int(x * (samples / self._width))
y_points.append(self._wave_table[table_idx])
# Update the final point
y_points[-1] = self._wave_table[-1]
# pylint: disable=nested-min-max
# Calculate the y-axis scale factor and adjust y values
self._max_sample_value = max(max(y_points), abs(min(y_points)))
if self._max_sample_value != 0:
if self._auto_scale:
self._scale_y = self._height / self._max_sample_value / 2
else:
self._scale_y = self._height / 32767 / 2
else:
self._scale_y = 1
for y in range(self._width):
y_points[y] = self._y_offset - int(y_points[y] * self._scale_y)
# Draw the values as an open polygon
bitmaptools.draw_polygon(
self._bmp,
x_points,
y_points,
1,
False,
)
def _plot_grid(self):
"""Plot the grid lines."""
# Draw the outer box
bitmaptools.draw_polygon(
self._bmp,
array("h", [0, self._width - 1, self._width - 1, 0]),
array("h", [0, 0, self._height - 1, self._height - 1]),
2,
)
if not self._envelope_plot:
# Draw x-axis line for wave plot
bitmaptools.draw_line(
self._bmp,
0,
self._y_offset,
self._width,
self._y_offset,
2,
)