# Copyright 2024 Allen Synthesis
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from europi_script import EuroPiScript
from time import sleep, sleep_ms, ticks_ms, ticks_diff
#Output a 10ms trigger to indicate end-of-rise and end-of-fall on CV5/6
#Constants for tracking the direction of the envelope's voltage
class EnvelopeGenerator(EuroPiScript):
state = self.load_state_json()
self.sustain_mode = state.get("sustain_mode", SUSTAIN_MODE_ASR)
self.looping_mode = state.get("looping_mode", LOOPING_MODE_ONCE)
#Milliscond tick of of the most recent end-of-rise and end-of-fall
#initialized to be 2x trigger duration in the past on startup to prevent roll-over issues
self.last_rise_end_at = time.ticks_add(time.ticks_ms(), -2*TRIGGER_DURATION_MS)
self.last_fall_end_at = self.last_rise_end_at
self.max_output_voltage = europi_config.MAX_OUTPUT_VOLTAGE
#Distance of envelope voltage from max voltage/0 before 'jumping' to it - prevents large logarithmic calculations
self.voltage_threshold = 0.1
#Length of the longest possible envelope (not in any meaningful unit)
self.max_increment_factor = 256
self.update_increment_factor()
#Time in ms between incrementing value of envelope
#0 will start a new envelope at the current value, 1 will start it from zero
#Display refresh rate in ms
self.display_refresh_rate = 30
self.last_refreshed_display = self.display_refresh_rate
self.envelope_display_bounds = [0, 0, int(OLED_WIDTH), int(OLED_HEIGHT / 2)]
self.direction = DIRECTION_SUSTAIN # DIRECTION_RISING, DIRECTION_FALLING, or DIRECTION_SUSTAIN
self.envelope_inverted_out = cv3
din.handler(self.receive_trigger_rise)
din.handler_falling(self.receive_trigger_fall)
b1.handler(self.change_sustain_mode)
b2.handler(self.change_looping_mode)
def receive_trigger_rise(self):
if self.retrig_mode == 1:
def receive_trigger_fall(self):
if self.direction == DIRECTION_RISING:
# Interrupted rise; output the trigger because we've risen as high as we're going to
self.last_rise_end_at = time.ticks_ms()
self.direction = DIRECTION_FALLING
def change_sustain_mode(self):
self.sustain_mode = 1 - self.sustain_mode
def change_looping_mode(self):
self.looping_mode = 1 - self.looping_mode
def copy_digital_input(self):
self.din_copy_out.value(din.value())
def difference(self, a, b):
def update_increment_factor(self):
increment_factor_rising = k1.range(self.max_increment_factor, 512) / 2
self.increment_factor = [(increment_factor_rising + 1), (k2.range(self.max_increment_factor, 256) + 1 + (ain.percent(256) * self.max_increment_factor))]
return log(max(number, 1))
def update_envelope_value(self):
if self.direction == DIRECTION_RISING:
increment = self.difference(self.envelope_value, self.max_output_voltage) / self.increment_factor[0]
self.envelope_value += increment
if self.difference(self.envelope_value, self.max_output_voltage) <= self.voltage_threshold:
self.envelope_value = self.max_output_voltage
if self.sustain_mode == 1 and self.looping_mode == 0:
self.direction = DIRECTION_SUSTAIN
self.direction = DIRECTION_FALLING
self.last_rise_end_at = time.ticks_ms()
sleep_ms(self.increment_delay)
elif self.direction == DIRECTION_FALLING:
increment = self.difference(0, self.envelope_value) / self.increment_factor[1]
self.envelope_value -= increment
if self.difference(0, self.envelope_value) <= self.voltage_threshold:
if self.looping_mode == LOOPING_MODE_ONCE:
self.direction = DIRECTION_SUSTAIN
self.direction = DIRECTION_RISING
self.last_fall_end_at = time.ticks_ms()
sleep_ms(self.increment_delay)
#Update CV output to envelope value
self.update_output_voltage()
def update_output_voltage(self):
self.envelope_out.voltage(self.envelope_value)
self.envelope_inverted_out.voltage(self.max_output_voltage - self.envelope_value)
if self.direction == DIRECTION_SUSTAIN:
if time.ticks_diff(now, self.last_rise_end_at) <= TRIGGER_DURATION_MS:
if time.ticks_diff(now, self.last_fall_end_at) <= TRIGGER_DURATION_MS:
def update_display(self):
if ticks_diff(ticks_ms(), self.last_refreshed_display) >= self.display_refresh_rate:
oled.hline(self.envelope_display_bounds[0], self.envelope_display_bounds[3], self.envelope_display_bounds[2], 1)
oled.vline(self.envelope_display_bounds[0], self.envelope_display_bounds[1], self.envelope_display_bounds[3], 1)
oled.vline((self.envelope_display_bounds[2] - 1), self.envelope_display_bounds[1], self.envelope_display_bounds[3], 1)
rise_width = (self.increment_factor[0] - 1) / ((self.increment_factor[0] - 1) + (self.increment_factor[1] - 1)) #If envelope has zero rise and zero fall this will throw a ZeroDivisonError
except ZeroDivisionError:
if draw_envelope == True:
rise_width_pixels = int(rise_width * self.envelope_display_bounds[2])
fall_width = 1 - rise_width
fall_width_pixels = int(self.envelope_display_bounds[2] - rise_width_pixels)
#Generate rise slope pixels
for pixel in range(rise_width_pixels):
x = pixel / (rise_width_pixels + 1)
x_pixel = rise_width_pixels - int(x * rise_width_pixels)
y_pixel = int(y * (self.envelope_display_bounds[3] - self.envelope_display_bounds[1]))
rise_pixels.append((x_pixel, y_pixel))
rise_pixels.append((self.envelope_display_bounds[0], self.envelope_display_bounds[3]))
#Generate fall slope pixels
for pixel in range(fall_width_pixels):
x = pixel / (fall_width_pixels + 1)
x_pixel = (fall_width_pixels - int(x * fall_width_pixels)) + rise_width_pixels
y_pixel = self.envelope_display_bounds[3] - int(y * (self.envelope_display_bounds[3] - self.envelope_display_bounds[1]))
fall_pixels.append((x_pixel, y_pixel))
fall_pixels.append((rise_width_pixels, self.envelope_display_bounds[1]))
#Draw rise and fall slopes
for array in [rise_pixels, fall_pixels]:
for index, pixel in enumerate(array[:-1]):
oled.line(array[index + 1][0], array[index + 1][1], pixel[0], pixel[1], 1)
#Draw current envelope position
current_envelope_position = 0
if self.direction == DIRECTION_RISING or self.direction == DIRECTION_SUSTAIN:
current_envelope_position = int((self.envelope_value / self.max_output_voltage) * rise_width_pixels)
elif self.direction == DIRECTION_FALLING:
current_envelope_position = self.envelope_display_bounds[2] - 1 - int((self.envelope_value / self.max_output_voltage) * (self.envelope_display_bounds[2] - rise_width_pixels))
oled.vline(current_envelope_position, self.envelope_display_bounds[1], (self.envelope_display_bounds[3] - 1), 1)
oled.vline(self.envelope_display_bounds[0], self.envelope_display_bounds[1], self.envelope_display_bounds[3], 1)
oled.hline(self.envelope_display_bounds[0], self.envelope_display_bounds[1], self.envelope_display_bounds[2], 1)
oled.vline((self.envelope_display_bounds[2] - 1), self.envelope_display_bounds[1], self.envelope_display_bounds[3], 1)
#Display current envelope direction
if self.direction == DIRECTION_RISING:
elif self.direction == DIRECTION_FALLING:
elif self.direction == DIRECTION_SUSTAIN and self.envelope_value == self.max_output_voltage:
oled.text(direction_text, 0, 20, 1)
#Display current envelope mode (AR or ASR)
if self.sustain_mode == SUSTAIN_MODE_AR:
sustain_mode_text = 'asr'
oled.text(sustain_mode_text, 50 + (4 if sustain_mode_text == 'ar' else 0), 20, 1)
#Display current envelope looping mode
if self.looping_mode == LOOPING_MODE_ONCE:
looping_mode_text = 'once'
looping_mode_text = 'loop'
oled.text(looping_mode_text, 94, 20, 1)
self.last_refreshed_display = ticks_ms()
"""Save the current state variables as JSON."""
# Don't save if it has been less than 5 seconds since last save.
if self.last_saved() < 5000:
"sustain_mode": self.sustain_mode,
"looping_mode": self.looping_mode,
self.save_state_json(state)
self.copy_digital_input()
self.update_increment_factor()
self.update_envelope_value()
if __name__ == "__main__":
EnvelopeGenerator().main()