I've trying to implement transition from an amount of space to another which is similar to acceleration and deceleration, except i failed and the only thing that i got from this was this infinite stack of mess, here is a screenshot showing this in action:
you can see a very black circle here, which are in reality something like 100 or 200 circles stacked on top of each other
and i reached this result using this piece of code:
def Place_circles(curve, circle_space, cs, draw=True, screen=None):
curve_acceleration = []
if type(curve) == tuple:
curve_acceleration = curve[1][0]
curve_intensity = curve[1][1]
curve = curve[0]
#print(curve_intensity)
#print(curve_acceleration)
Circle_list = []
idx = [0,0]
for c in reversed(range(0,len(curve))):
for p in reversed(range(0,len(curve[c]))):
user_dist = circle_space[curve_intensity[c]] + curve_acceleration[c] * p
dist = math.sqrt(math.pow(curve[c][p][0] - curve[idx[0]][idx[1]][0],2)+math.pow(curve [c][p][1] - curve[idx[0]][idx[1]][1],2))
if dist > user_dist:
idx = [c,p]
Circle_list.append(circles.circles(round(curve[c][p][0]), round(curve[c][p][1]), cs, draw, screen))
This place circles depending on the intensity (a number between 0 and 2, random) of the current curve, which equal to an amount of space (let's say between 20 and 30 here, 20 being index 0, 30 being index 2 and a number between these 2 being index 1).
This create the stack you see above and isn't what i want, i also came to the conclusion that i cannot use acceleration since the amount of time to move between 2 points depend on the amount of circles i need to click on, knowing that there are multiple circles between each points, but not being able to determine how many lead to me being unable to the the classic acceleration formula.
So I'm running out of options here and ideas on how to transition from an amount of space to another.
any idea?
PS: i scrapped the idea above and switched back to my master branch but the code for this is still available in the branch i created here https://github.com/Mrcubix/Osu-StreamGenerator/tree/acceleration .
So now I'm back with my normal code that don't possess acceleration or deceleration.
TL:DR i can't use acceleration since i don't know the amount of circles that are going to be placed between the 2 points and make the time of travel vary (i need for exemple to click circles at 180 bpm of one circle every 0.333s) so I'm looking for another way to generate gradually changing space.
First, i took my function that was generating the intensity for each curves in [0 ; 2]
Then i scrapped the acceleration formula as it's unusable.
Now i'm using a basic algorithm to determine the maximum amount of circles i can place on a curve.
Now the way my script work is the following:
i first generate a stream (multiple circles that need to be clicked at high bpm)
this way i obtain the length of each curves (or segments) of the polyline.
i generate an intensity for each curve using the following function:
def generate_intensity(Circle_list: list = None, circle_space: int = None, Args: list = None):
curve_intensity = []
if not Args or Args[0] == "NewProfile":
prompt = True
while prompt:
max_duration_intensity = input("Choose the maximum amount of curve the change in intensity will occur for: ")
if max_duration_intensity.isdigit():
max_duration_intensity = int(max_duration_intensity)
prompt = False
prompt = True
while prompt:
intensity_change_odds = input("Choose the odds of occurence for changes in intensity (1-100): ")
if intensity_change_odds.isdigit():
intensity_change_odds = int(intensity_change_odds)
if 0 < intensity_change_odds <= 100:
prompt = False
prompt = True
while prompt:
min_intensity = input("Choose the lowest amount of spacing a circle will have: ")
if min_intensity.isdigit():
min_intensity = float(min_intensity)
if min_intensity < circle_space:
prompt = False
prompt = True
while prompt:
max_intensity = input("Choose the highest amount of spacing a circle will have: ")
if max_intensity.isdigit():
max_intensity = float(max_intensity)
if max_intensity > circle_space:
prompt = False
prompt = True
if Args:
if Args[0] == "NewProfile":
return [max_duration_intensity, intensity_change_odds, min_intensity, max_intensity]
elif Args[0] == "GenMap":
max_duration_intensity = Args[1]
intensity_change_odds = Args[2]
min_intensity = Args[3]
max_intensity = Args[4]
circle_space = ([min_intensity, circle_space, max_intensity] if not Args else [Args[0][3],circle_space,Args[0][4]])
count = 0
for idx, i in enumerate(Circle_list):
if idx == len(Circle_list) - 1:
if random.randint(0,100) < intensity_change_odds:
if random.randint(0,100) > 50:
curve_intensity.append(2)
else:
curve_intensity.append(0)
else:
curve_intensity.append(1)
if random.randint(0,100) < intensity_change_odds:
if random.randint(0,100) > 50:
curve_intensity.append(2)
count += 1
else:
curve_intensity.append(0)
count += 1
else:
if curve_intensity:
if curve_intensity[-1] == 2 and not count+1 > max_duration_intensity:
curve_intensity.append(2)
count += 1
continue
elif curve_intensity[-1] == 0 and not count+1 > max_duration_intensity:
curve_intensity.append(0)
count += 1
continue
elif count+1 > 2:
curve_intensity.append(1)
count = 0
continue
else:
curve_intensity.append(1)
else:
curve_intensity.append(1)
curve_intensity.reverse()
if curve_intensity.count(curve_intensity[0]) == len(curve_intensity):
print("Intensity didn't change")
return circle_space[1]
print("\n")
return [circle_space, curve_intensity]
with this, i obtain 2 list, one with the spacing i specified, and the second one is the list of randomly generated intensity.
from there i call another function taking into argument the polyline, the previously specified spacings and the generated intensity:
def acceleration_algorithm(polyline, circle_space, curve_intensity):
new_circle_spacing = []
for idx in range(len(polyline)): #repeat 4 times
spacing = []
Length = 0
best_spacing = 0
for p_idx in range(len(polyline[idx])-1): #repeat 1000 times / p_idx in [0 ; 1000]
# Create multiple list containing spacing going from circle_space[curve_intensity[idx-1]] to circle_space[curve_intensity[idx]]
spacing.append(np.linspace(circle_space[curve_intensity[idx]],circle_space[curve_intensity[idx+1]], p_idx).tolist())
# Sum distance to find length of curve
Length += abs(math.sqrt((polyline[idx][p_idx+1][0] - polyline[idx][p_idx][0]) ** 2 + (polyline [idx][p_idx+1][1] - polyline[idx][p_idx][1]) ** 2))
for s in range(len(spacing)): # probably has 1000 list in 1 list
length_left = Length # Make sure to reset length for each iteration
for dist in spacing[s]: # substract the specified int in spacing[s]
length_left -= dist
if length_left > 0:
best_spacing = s
else: # Since length < 0, use previous working index (best_spacing), could also jsut do `s-1`
if spacing[best_spacing] == []:
new_circle_spacing.append([circle_space[1]])
continue
new_circle_spacing.append(spacing[best_spacing])
break
return new_circle_spacing
with this, i obtain a list with the space between each circles that are going to be placed,
from there, i can Call Place_circles() again, and obtain the new stream:
def Place_circles(polyline, circle_space, cs, DoDrawCircle=True, surface=None):
Circle_list = []
curve = []
next_circle_space = None
dist = 0
for c in reversed(range(0, len(polyline))):
curve = []
if type(circle_space) == list:
iter_circle_space = iter(circle_space[c])
next_circle_space = next(iter_circle_space, circle_space[c][-1])
for p in reversed(range(len(polyline[c])-1)):
dist += math.sqrt((polyline[c][p+1][0] - polyline[c][p][0]) ** 2 + (polyline [c][p+1][1] - polyline[c][p][1]) ** 2)
if dist > (circle_space if type(circle_space) == int else next_circle_space):
dist = 0
curve.append(circles.circles(round(polyline[c][p][0]), round(polyline[c][p][1]), cs, DoDrawCircle, surface))
if type(circle_space) == list:
next_circle_space = next(iter_circle_space, circle_space[c][-1])
Circle_list.append(curve)
return Circle_list
the result is a stream with varying space between circles (so accelerating or decelerating), the only issue left to be fixed is pygame not updating the screen with the new set of circle after i call Place_circles(), but that's an issue i'm either going to try to fix myself or ask in another post
the final code for this feature can be found on my repo : https://github.com/Mrcubix/Osu-StreamGenerator/tree/Acceleration_v02
For the past few weeks I have been working on a project which is all very new to me, and I'm learning as I go. I'm building a synthesizer using a Raspberry Pi 2 and I'm coding it in Python3, as I have some basic knowledge of the language, but not much real experience. I've muddled through pretty well so far, but I have now hit the wall I knew I would hit eventually: performance.
I have been using Pygame and its Sound module to create the sounds I want, and then using my own mathematical algorithms to calculate the ADS(H)R volume envelope for every sound. I tweak this envelope using 8 potentiometers. 3 of them control the length in seconds of the Attack, Decay, Release and another one to set the Sustain level. Then I added 4 more pots which control the curvature of each part of the envelope (except one of them instead sets a Hold value for Sustain). I have a PiTFT screen connected as well which draws up the current shape and length of the entire envelope, as well as prints out the current values of ADSR.
To play sounds I use a 4x4 Adafruit Trellis board and with different button combinations I can play every note between C0 and C8.
I use SciPy and NumPy to create different kinds of soundwaves, as in Sine, Square, Triangle, Sawtooth, Pulse and Noise.
As I have been using regular for loops to change the volume of the sound according to the ADSR envelope, running the function PlaySound takes a while to complete (depending on my ADSR settings of course). This prompted me to try using threads. I don't know if I'm using it in the best way, of if I should use it at all, but it was the only way I could think of to achieve polyphony. Otherwise it had to wait until a sound was completed until it would resume the main loop. So now I can play several notes at the same time. Well, two notes at least. After that it lags and the third one doesn't seem to play until one of the previous sounds have finished.
I've done some tests and checks and I should be able to runt up to 4 threads at the same time, but I might be missing something. One guess is that the system itself has reserved two threads (cores) for other usage.
I realize also that Python is not the most efficient language to use, and I've been looking into Pure Data as well, but I'm having trouble wrapping my head around it (I prefer code over a click-and-drag-gui). I want to keep using Python for as long as possible. I might look into using pyo, but I think I'd have to mostly start from scratch with my code then (which I am willing to do, but I don't want to give up on my current code just yet).
So. Here's my question(s): How can I optimize this to be truly polyphonic? Two notes is not enough. Should I skip the threads altogether? Can I implement the ADSR envelope in a better, less costly way? How can I clean up my messy math? What other performance bottlenecks are there, that I have overlooked? The Pygame drawing to the screen seems to be negligable at the moment, as there is virtually no difference at all if I disable it completely. Here is my code so far:
import pygame
from pygame.mixer import Sound, get_init, pre_init, get_num_channels
from array import array
import RPi.GPIO as GPIO
import alsaaudio
import time
import Adafruit_Trellis
import Adafruit_MCP3008
import math
import _thread
import os
import multiprocessing
import numpy as np
from scipy import signal as sg
import struct
#print(str(multiprocessing.cpu_count()))
os.putenv('SDL_FBDEV','/dev/fb1')
fps = pygame.time.Clock()
FRAMERATE = 100
MINSEC = 1/FRAMERATE
BLUE = ( 0, 0, 255)
WHITE = (255, 255, 255)
DARKRED = (128, 0, 0)
DARKBLUE = ( 0, 0, 128)
RED = (255, 0, 0)
GREEN = ( 0, 255, 0)
DARKGREEN = ( 0, 128, 0)
YELLOW = (255, 255, 0)
DARKYELLOW = (128, 128, 0)
BLACK = ( 0, 0, 0)
PTCH = [ 1.00, 1.059633027522936, 1.122324159021407, 1.18960244648318,
1.259938837920489, 1.335168195718654, 1.414067278287462,
1.498470948012232, 1.587767584097859, 1.681957186544343,
1.782262996941896, 1.888073394495413, 2.00 ]
FREQ = { # Parsed from http://www.phy.mtu.edu/~suits/notefreqs.html
'C0': 16.35, 'Cs0': 17.32, 'D0': 18.35, 'Ds0': 19.45, 'E0': 20.60,
'F0': 21.83, 'Fs0': 23.12, 'G0': 24.50, 'Gs0': 25.96, 'A0': 27.50,
'As0': 29.14, 'B0': 30.87, 'C1': 32.70, 'Cs1': 34.65, 'D1': 36.71,
'Ds1': 38.89, 'E1': 41.20, 'F1': 43.65, 'Fs1': 46.25, 'G1': 49.00,
'Gs1': 51.91, 'A1': 55.00, 'As1': 58.27, 'B1': 61.74, 'C2': 65.41,
'Cs2': 69.30, 'D2': 73.42, 'Ds2': 77.78, 'E2': 82.41, 'F2': 87.31,
'Fs2': 92.50, 'G2': 98.00, 'Gs2': 103.83, 'A2': 110.00, 'As2': 116.54,
'B2': 123.47, 'C3': 130.81, 'Cs3': 138.59, 'D3': 146.83, 'Ds3': 155.56,
'E3': 164.81, 'F3': 174.61, 'Fs3': 185.00, 'G3': 196.00, 'Gs3': 207.65,
'A3': 220.00, 'As3': 233.08, 'B3': 246.94, 'C4': 261.63, 'Cs4': 277.18,
'D4': 293.66, 'Ds4': 311.13, 'E4': 329.63, 'F4': 349.23, 'Fs4': 369.99,
'G4': 392.00, 'Gs4': 415.30, 'A4': 440.00, 'As4': 466.16, 'B4': 493.88,
'C5': 523.25, 'Cs5': 554.37, 'D5': 587.33, 'Ds5': 622.25, 'E5': 659.26,
'F5': 698.46, 'Fs5': 739.99, 'G5': 783.99, 'Gs5': 830.61, 'A5': 880.00,
'As5': 932.33, 'B5': 987.77, 'C6': 1046.50, 'Cs6': 1108.73, 'D6': 1174.66,
'Ds6': 1244.51, 'E6': 1318.51, 'F6': 1396.91, 'Fs6': 1479.98, 'G6': 1567.98,
'Gs6': 1661.22, 'A6': 1760.00, 'As6': 1864.66, 'B6': 1975.53, 'C7': 2093.00,
'Cs7': 2217.46, 'D7': 2349.32, 'Ds7': 2489.02, 'E7': 2637.02, 'F7': 2793.83,
'Fs7': 2959.96, 'G7': 3135.96, 'Gs7': 3322.44, 'A7': 3520.00,
'As7': 3729.31, 'B7': 3951.07,
'C8': 4186.01, 'Cs8': 4434.92, 'D8': 4698.64, 'Ds8': 4978.03,
}
buttons = ['A',PTCH[9],PTCH[10],PTCH[11],'B',PTCH[6],PTCH[7],PTCH[8],'C',PTCH[3],PTCH[4],PTCH[5],PTCH[12],PTCH[0],PTCH[1],PTCH[2] ]
octaves = { 'BASE':'0', 'A':'1', 'B':'2', 'C':'3', 'AB':'4', 'AC':'5', 'BC':'6', 'ABC':'7' }
class Note(pygame.mixer.Sound):
def __init__(self, frequency, volume=.1):
self.frequency = frequency
self.oktostop = False
Sound.__init__(self, self.build_samples())
self.set_volume(volume)
def playSound(self, Aval, Dval, Sval, Rval, Acurve, Dcurve, Shold, Rcurve, fps):
self.set_volume(0)
self.play(-1)
if Aval >= MINSEC:
Alength = round(Aval*FRAMERATE)
for num in range(0,Alength+1):
fps.tick_busy_loop(FRAMERATE)
volume = (Acurve[1]*pow(num*MINSEC,Acurve[0]))/100
self.set_volume(volume)
#print(fps.get_time()," ",str(volume))
else:
self.set_volume(100)
if Sval <= 1 and Sval > 0 and Dval >= MINSEC:
Dlength = round(Dval*FRAMERATE)
for num in range(0,Dlength+1):
fps.tick_busy_loop(FRAMERATE)
volume = (Dcurve[1]*pow(num*MINSEC,Dcurve[0])+100)/100
self.set_volume(volume)
#print(fps.get_time()," ",str(volume))
elif Sval <= 1 and Sval > 0 and Dval < MINSEC:
self.set_volume(Sval)
else:
self.set_volume(0)
if Shold >= MINSEC:
Slength = round(Shold*FRAMERATE)
for num in range(0,Slength+1):
fps.tick_busy_loop(FRAMERATE)
while True:
if self.oktostop:
if Sval > 0 and Rval >= MINSEC:
Rlength = round(Rval*FRAMERATE)
for num in range(0,Rlength+1):
fps.tick_busy_loop(FRAMERATE)
volume = (Rcurve[1]*pow(num*MINSEC,Rcurve[0])+(Sval*100))/100
self.set_volume(volume)
#print(fps.get_time()," ",str(volume))
self.stop()
break
def stopSound(self):
self.oktostop = True
def build_samples(self):
Fs = get_init()[0]
f = self.frequency
sample = Fs/f
x = np.arange(sample)
# Sine wave
#y = 0.5*np.sin(2*np.pi*f*x/Fs)
# Square wave
y = 0.5*sg.square(2*np.pi*f*x/Fs)
# Pulse wave
#sig = np.sin(2 * np.pi * x)
#y = 0.5*sg.square(2*np.pi*f*x/Fs, duty=(sig + 1)/2)
# Sawtooth wave
#y = 0.5*sg.sawtooth(2*np.pi*f*x/Fs)
# Triangle wave
#y = 0.5*sg.sawtooth(2*np.pi*f*x/Fs,0.5)
# White noise
#y = 0.5*np.random.uniform(-1.000,1.000,sample)
return y
pre_init(44100, -16, 2, 2048)
pygame.init()
screen = pygame.display.set_mode((480, 320))
pygame.mouse.set_visible(False)
CLK = 5
MISO = 6
MOSI = 13
CS = 12
mcp = Adafruit_MCP3008.MCP3008(clk=CLK, cs=CS, miso=MISO, mosi=MOSI)
Asec = 1.0
Dsec = 1.0
Ssec = 1.0
Rsec = 1.0
matrix0 = Adafruit_Trellis.Adafruit_Trellis()
trellis = Adafruit_Trellis.Adafruit_TrellisSet(matrix0)
NUMTRELLIS = 1
numKeys = NUMTRELLIS * 16
I2C_BUS = 1
trellis.begin((0x70, I2C_BUS))
# light up all the LEDs in order
for i in range(int(numKeys)):
trellis.setLED(i)
trellis.writeDisplay()
time.sleep(0.05)
# then turn them off
for i in range(int(numKeys)):
trellis.clrLED(i)
trellis.writeDisplay()
time.sleep(0.05)
posRecord = {'attack': [], 'decay': [], 'sustain': [], 'release': []}
octaval = {'A':False,'B':False,'C':False}
pitch = 0
tone = None
old_tone = None
note = None
volume = 0
#m = alsaaudio.Mixer('PCM')
#mastervol = m.getvolume()
sounds = {}
values = [0]*8
oldvalues = [0]*8
font = pygame.font.SysFont("comicsansms", 22)
while True:
fps.tick_busy_loop(FRAMERATE)
#print(fps.get_time())
update = False
#m.setvolume(int(round(MCP3008(4).value*100)))
#mastervol = m.getvolume()
values = [0]*8
for i in range(8):
# The read_adc function will get the value of the specified channel (0-7).
values[i] = mcp.read_adc(i)/1000
if values[i] >= 1:
values[i] = 1
# Print the ADC values.
#print('| {0:>4} | {1:>4} | {2:>4} | {3:>4} | {4:>4} | {5:>4} | {6:>4} | {7:>4} |'.format(*values))
#print(str(pygame.mixer.Channel(0).get_busy())+" "+str(pygame.mixer.Channel(1).get_busy())+" "+str(pygame.mixer.Channel(2).get_busy())+" "+str(pygame.mixer.Channel(3).get_busy())+" "+str(pygame.mixer.Channel(4).get_busy())+" "+str(pygame.mixer.Channel(5).get_busy())+" "+str(pygame.mixer.Channel(6).get_busy())+" "+str(pygame.mixer.Channel(7).get_busy()))
Sval = values[2]*Ssec
Aval = values[0]*Asec
if Sval == 1:
Dval = 0
else:
Dval = values[1]*Dsec
if Sval < MINSEC:
Rval = 0
else:
Rval = values[3]*Rsec
if Aval > 0:
if values[4] <= MINSEC: values[4] = MINSEC
Acurve = [round(values[4]*4,3),round(100/pow(Aval,(values[4]*4)),3)]
else:
Acurve = False
if Dval > 0:
if values[5] <= MINSEC: values[5] = MINSEC
Dcurve = [round(values[5]*4,3),round(((Sval*100)-100)/pow(Dval,(values[5]*4)),3)]
else:
Dcurve = False
Shold = values[6]*4*Ssec
if Rval > 0 and Sval > 0:
if values[7] <= MINSEC: values[7] = MINSEC
Rcurve = [round(values[7]*4,3),round(-Sval*100/pow(Rval,(values[7]*4)),3)]
else:
Rcurve = False
if update:
screen.fill((0, 0, 0))
scrnvals = ["A: "+str(round(Aval,2))+"s","D: "+str(round(Dval,2))+"s","S: "+str(round(Sval,2)),"R: "+str(round(Rval,2))+"s","H: "+str(round(Shold,2))+"s","ENV: "+str(round(Aval,2)+round(Dval,2)+round(Shold,2)+round(Rval,2))+"s"]
for line in range(len(scrnvals)):
text = font.render(scrnvals[line], True, (0, 128, 0))
screen.blit(text,(60*line+40, 250))
# Width of one second in number of pixels
ASCALE = 20
DSCALE = 20
SSCALE = 20
RSCALE = 20
if Aval >= MINSEC:
if Aval <= 1:
ASCALE = 80
else:
ASCALE = 20
# Attack
for yPos in range(0,101):
xPos = round(pow((yPos/Acurve[1]),(1/Acurve[0]))*ASCALE)
posRecord['attack'].append((int(xPos) + 40, int(-yPos) + 130))
if len(posRecord['attack']) > 1:
pygame.draw.lines(screen, DARKRED, False, posRecord['attack'], 2)
if Dval >= MINSEC:
if Dval <= 1:
DSCALE = 80
else:
DSCALE = 20
# Decay
for yPos in range(100,round(Sval*100)-1,-1):
xPos = round(pow(((yPos-100)/Dcurve[1]),(1/Dcurve[0]))*DSCALE)
#print(str(yPos)+" = "+str(Dcurve[1])+"*"+str(xPos)+"^"+str(Dcurve[0])+"+100")
posRecord['decay'].append((int(xPos) + 40 + round(Aval*ASCALE), int(-yPos) + 130))
if len(posRecord['decay']) > 1:
pygame.draw.lines(screen, DARKGREEN, False, posRecord['decay'], 2)
# Sustain
if Shold >= MINSEC:
for xPos in range(0,round(Shold*SSCALE)):
posRecord['sustain'].append((int(xPos) + 40 + round(Aval*ASCALE) + round(Dval*DSCALE), int(100-Sval*100) + 30))
if len(posRecord['sustain']) > 1:
pygame.draw.lines(screen, DARKYELLOW, False, posRecord['sustain'], 2)
if Rval >= MINSEC:
if Rval <= 1:
RSCALE = 80
else:
RSCALE = 20
# Release
for yPos in range(round(Sval*100),-1,-1):
xPos = round(pow(((yPos-round(Sval*100))/Rcurve[1]),(1/Rcurve[0]))*RSCALE)
#print(str(xPos)+" = (("+str(yPos)+"-"+str(round(Sval*100))+")/"+str(Rcurve[1])+")^(1/"+str(Rcurve[0])+")")
posRecord['release'].append((int(xPos) + 40 + round(Aval*ASCALE) + round(Dval*DSCALE) + round(Shold*SSCALE), int(-yPos) + 130))
if len(posRecord['release']) > 1:
pygame.draw.lines(screen, DARKBLUE, False, posRecord['release'], 2)
posRecord = {'attack': [], 'decay': [], 'sustain': [], 'release': []}
pygame.display.update()
tone = None
pitch = 0
time.sleep(MINSEC)
# If a button was just pressed or released...
if trellis.readSwitches():
# go through every button
for i in range(numKeys):
# if it was pressed, turn it on
if trellis.justPressed(i):
print('v{0}'.format(i))
trellis.setLED(i)
if i == 0:
octaval['A'] = True
elif i == 4:
octaval['B'] = True
elif i == 8:
octaval['C'] = True
else:
pitch = buttons[i]
button = i
# if it was released, turn it off
if trellis.justReleased(i):
print('^{0}'.format(i))
trellis.clrLED(i)
if i == 0:
octaval['A'] = False
elif i == 4:
octaval['B'] = False
elif i == 8:
octaval['C'] = False
else:
sounds[i].stopSound()
# tell the trellis to set the LEDs we requested
trellis.writeDisplay()
octa = ''
if octaval['A']:
octa += 'A'
if octaval['B']:
octa += 'B'
if octaval['C']:
octa += 'C'
if octa == '':
octa = 'BASE'
if pitch > 0:
tone = FREQ['C0']*pow(2,int(octaves[octa]))*pitch
if tone:
sounds[button] = Note(tone)
_thread.start_new_thread(sounds[button].playSound,(Aval, Dval, Sval, Rval, Acurve, Dcurve, Shold, Rcurve, fps))
print(str(tone))
GPIO.cleanup()
what you are doing at the moment, is firing a sound and giving up all control, until that sound has been played. The general approach here would be to change that and process one sample at a time and push that to a buffer, that is played back periodicaly. That sample would be a sum of all your voices/signals. That way, you can decide for every sample, if a new voice is to be triggered and you can decide how long to play a note while already playing it. One way to do this would be to install a timer, that triggers a callback-function every 1/48000 s if you want a samplingrate of 48kHz.
You could still use multithreading for parallel processing, if you need to process a lot of voices, but not one thread for one voice, that would be overkill in my opinions. If that is nescessary or not depends on how much filtering/processing you do and how effective/ineffective your program is.
e.g.
sample_counter = 0
output_buffer = list()
def callback_fct():
pitch_0 = 2
pitch_1 = 4
sample_counter += 1 #time in ms
signal_0 = waveform(sample_counter * pitch_0)
signal_1 = waveform(sample_counter * pitch_1)
signal_out = signal_0 * 0.5 + signal_1 *0.5
output_buffer.append(signal_out)
return 0
if __name__ == "__main__":
call_this_function_every_ms(callback_fct)
play_sound_from_outputbuffer() #plays sound from outputbuffer by popping samples from the beginning of the list.
Something like that. the waveform() function would give you sample-values based on the actual time times the desired pitch. In C you would do all that with pointers, that overflow at the end of the Wavetable, so you won't have to deal with the question, when you should reset your sample_counter without getting glitches in the waveform (it will get real big realy soon). But I am shure, there are more "pythonic" aproaches to that. Another good reason to do this in a more low level language is speed. As soon as you involve real DSP, you will count your processor clock ticks. At that point python may just have too much overhead.
You are right that python is probably one of the bottlenecks. Commercial soft-synths are, almost without exception, written in C++ to leverage all kinds of optimization - the most pertinent of these is use of vector processing units.
There are, nonetheless, plenty of optimizations open to you in Python:
You are calculating the envelope every sample, and in an expensive way (using pow() - which is not totally hardware accelerated on ARM Cortex CPUs. You can potentially pre-compute the transfer function and simply multiply this with each sample. I also suspect that at 44.1kHz or higher, you don't need to change the envelope every sample - perhaps every 100 or so is good enough.
Your oscillators are also calculated per-sample, and as far as I can tell, per note playback. Some of them are fairly cheap, but trig functions less so, Practical soft-synths use oscillator wave-tables and phase-accumulator as an approximation.
Things you have less control of
Accuracy: You are ultimately generating a 16-bit sample. I suspect that by default Python is using double precision for everything - which has a 48-bit mantissa - about 3 times wider than you need.
Double-precision maths functions are slow on ARM Cortex A parts - significantly so in fact. Single precision can go via the VPU with many operations you would use a lot in DSP such as MAC (multiply-accumulate) taking a single cycle (although they take something like 16 cycles to clear the pipeline). Double precision is orders of magnitude slower.
#Rantanplan's answer above alludes to the kind of software architecture soft-synths are built with - one which is event driven, with a render-handler called upon periodically to supply samples. A polyphonic softsynth an do these in parallel.
In a well optimized implementation the processing of each sample for each voice would involve:
* One lookup from the wave-table (having first calculated the buffer offset using integer maths)
* multiplication by the envelope
* Mix the sample with others in the output buffer.
The key to performance is that there are almost no flow control statements in this tight loop.
Periodically, possibly per callback interval, the envelope would be updated. This parallelizes for several adjacent samples at once on CPUs with VPUs - so that would be two-ways on an ARM Cortex A part.
My problem is as follows:
having file with list of intervals:
1 5
2 8
9 12
20 30
And a range of
0 200
I would like to do such an intersection that will report the positions [start end] between my intervals inside the given range.
For example:
8 9
12 20
30 200
Beside any ideas how to bite this, would be also nice to read some thoughts on optimization, since as always the input files are going to be huge.
this solution works as long the intervals are ordered by the start point and does not require to create a list as big as the total range.
code
with open("0.txt") as f:
t=[x.rstrip("\n").split("\t") for x in f.readlines()]
intervals=[(int(x[0]),int(x[1])) for x in t]
def find_ints(intervals, mn, mx):
next_start = mn
for x in intervals:
if next_start < x[0]:
yield next_start,x[0]
next_start = x[1]
elif next_start < x[1]:
next_start = x[1]
if next_start < mx:
yield next_start, mx
print list(find_ints(intervals, 0, 200))
output:
(in the case of the example you gave)
[(0, 1), (8, 9), (12, 20), (30, 200)]
Rough algorithm:
create an array of booleans, all set to false seen = [False]*200
Iterate over the input file, for each line start end set seen[start] .. seen[end] to be True
Once done, then you can trivially walk the array to find the unused intervals.
In terms of optimisations, if the list of input ranges is sorted on start number, then you can track the highest seen number and use that to filter ranges as they are processed -
e.g. something like
for (start,end) in input:
if end<=lowest_unseen:
next
if start<lowest_unseen:
start=lowest_unseen
...
which (ignoring the cost of the original sort) should make the whole thing O(n) - you go through the array once to tag seen/unseen and once to output unseens.
Seems I'm feeling nice. Here is the (unoptimised) code, assuming your input file is called input
seen = [False]*200
file = open('input','r')
rows = file.readlines()
for row in rows:
(start,end) = row.split(' ')
print "%s %s" % (start,end)
for x in range( int(start)-1, int(end)-1 ):
seen[x] = True
print seen[0:10]
in_unseen_block=False
start=1
for x in range(1,200):
val=seen[x-1]
if val and not in_unseen_block:
continue
if not val and in_unseen_block:
continue
# Must be at a change point.
if val:
# we have reached the end of the block
print "%s %s" % (start,x)
in_unseen_block = False
else:
# start of new block
start = x
in_unseen_block = True
# Handle end block
if in_unseen_block:
print "%s %s" % (start, 200)
I'm leaving the optimizations as an exercise for the reader.
If you make a note every time that one of your input intervals either opens or closes, you can do what you want by putting together the keys of opens and closes, sort into an ordered set, and you'll be able to essentially think, "okay, let's say that each adjacent pair of numbers forms an interval. Then I can focus all of my logic on these intervals as discrete chunks."
myRange = range(201)
intervals = [(1,5), (2,8), (9,12), (20,30)]
opens = {}
closes = {}
def open(index):
if index not in opens:
opens[index] = 0
opens[index] += 1
def close(index):
if index not in closes:
closes[index] = 0
closes[index] += 1
for start, end in intervals:
if end > start: # Making sure to exclude empty intervals, which can be problematic later
open(start)
close(end)
# Sort all the interval-endpoints that we really need to look at
oset = {0:None, 200:None}
for k in opens.keys():
oset[k] = None
for k in closes.keys():
oset[k] = None
relevant_indices = sorted(oset.keys())
# Find the clear ranges
state = 0
results = []
for i in range(len(relevant_indices) - 1):
start = relevant_indices[i]
end = relevant_indices[i+1]
start_state = state
if start in opens:
start_state += opens[start]
if start in closes:
start_state -= closes[start]
end_state = start_state
if end in opens:
end_state += opens[end]
if end in closes:
end_state -= closes[end]
state = end_state
if start_state == 0:
result_start = start
result_end = end
results.append((result_start, result_end))
for start, end in results:
print(str(start) + " " + str(end))
This outputs:
0 1
8 9
12 20
30 200
The intervals don't need to be sorted.
This question seems to be a duplicate of Merging intervals in Python.
If I understood well the problem, you have a list of intervals (1 5; 2 8; 9 12; 20 30) and a range (0 200), and you want to get the positions outside your intervals, but inside given range. Right?
There's a Python library that can help you on that: python-intervals (also available from PyPI using pip). Disclaimer: I'm the maintainer of that library.
Assuming you import this library as follows:
import intervals as I
It's quite easy to get your answer. Basically, you first want to create a disjunction of intervals based on the ones you provide:
inters = I.closed(1, 5) | I.closed(2, 8) | I.closed(9, 12) | I.closed(20, 30)
Then you compute the complement of these intervals, to get everything that is "outside":
compl = ~inters
Then you create the union with [0, 200], as you want to restrict the points to that interval:
print(compl & I.closed(0, 200))
This results in:
[0,1) | (8,9) | (12,20) | (30,200]