Python/Musical intervals (numpy matplotlib)/playsound
Appearance
This code uses the module playsound to create the WAV sound files located at:
Example: The OGG file shown to the left plays the (perfect) fifth, detuned from 2.16 cents in order to maintain a beat frequency of 1.5 Hz. The WAV file was converted to an OGG file using Audacity.
Installing the playsound module
[edit | edit source]I wanted to hear to interval each time I ran to code, so I installed the playsound using:
I found it necessary to install the previous version, as per this discussion on GitHub. The following code fails on the current version of playsound, but works if I rename the file to test.wav
. My guess is that the dashes confound the current version of playsound
(but I'm not sure.)
import playsound#---------Used old version: pip install playsound==1.2.2
playsound.playsound('fifth-300.0-200.25.wav')
Code
[edit | edit source]
def ifun(interval):
import numpy as np #------------------ numerical operations on large arrays
import matplotlib.pyplot as plt #----- creates and displays plots
import scipy, sys,os, time, math #---- scipy not used here
from scipy import signal #------------ signal not used here
import soundfile as sf #-------------- soundfile not used yet
import playsound#--------------------- old version: pip install playsound==1.2.2
## global variables #########################################################
global PI; PI=np.pi
global samplerate; samplerate=44100#-- standard CD sample rate
global dt; dt=1/samplerate#----------- dt defined
global docmate
docmate =r'''My journal for this code is at:
docs.google.com/document/d/1hb2NzXxMTkkjhEmckZftRuceN4FsaifUvaEoOxFa7uw
G:\My Drive\python\Intervals'''+"\n"
def drint(string):#----------------- maintains docmate journal
global docmate; docmate+= string+"\n"; return
def time2int(t):#------------------- converts time to an integer
global samplerate; return(round(t*samplerate))
def int2time(i):#------------------- converts an integer to time
global dt; return(i*dt)
start_time = time.time()#----------- code runtime measurement
## INPUT PARAMETERS ########################################################
makeplot, makesound = True, True#--- realtime, show/hear plot/sound
clearOut, shortTest = False, False#--- clears output, performs short segement
standAlone = False#------------------ standAlone runs from here
if standAlone: interval="fifth"#---- ... else run from pickInterval.py
Amp, clickT, clickA = .25, .005, .25#- Amplitude, click duration, click amplitude
## clickmap=[2,3,3,1,1]-> sscccssscs where s/c=silence/click if bar=1
clickmap, bar =[4,4,4,1,0], 3#----- bar = beats/musical bar (measure)
if shortTest: #----- clickmap, bar =[4,4,4,1,0], 3 (standard)
clickmap, bar =[0,1,1,1,0], 1#--for testing purposes only
dfstr = "q"# -----------------------selects tone (p/q) to be shifted
f_b, f_q = 1.5, 200#-------------- f_beat, f_q = low frequency tone
clickOctave=2#2#------------------- click is clickOctave(s) above p*q*f_0
highclick=1.5#--------------------- high-freq click us up a perfect fifth
## Modify clickmap if bar>1: ################################################
for i in range(len(clickmap)):clickmap[i]=clickmap[i]*bar
## Define Nbeats and ifclick, eg: clickmap=(2,3,3,1,1) => ifclick=(0,1,0,1,0)
Nbeats=0; do_click=False; ifclick=[]#---- do_click = "do click" (True/False)
for i in range(len(clickmap)):
Nbeats+=clickmap[i]
ifclick.append(do_click)#------------ uses fact that clickmap alternates
do_click=not(do_click)#--------------- between click and no click
## CODE SELECTS p,q, f_0=1/T_0, f_p, Df, cents, tmax, datapoints #############
if interval=="fifth":#--------------i=0
p,q,beatFactor = 3,2,2
if interval=="Maj 6th":#------------i=1
p,q,beatFactor = 5,3,1
if interval=="fourth":#-------------i=2
p,q,beatFactor = 4,3,2
if interval=="Maj 3rd":#------------i=3
p,q,beatFactor = 5,4,2
if interval=="min 6th":#------------i=4
p,q,beatFactor = 8,5,2
if interval=="min 3rd":#------------i=5
p,q,beatFactor = 6,5,2
if interval=="tritone":#------------i=6
p,q,beatFactor = 7,5,1
## Create plotlabel ##########################################################
plotlabel=interval+": "+str(bar)+ f" beats per bar - "
plotlabel+=f"{60/f_b:.2f} beats per second - "
plotlabel+=f"{clickmap[2]/bar:.1f} bar rest"
## CALCULATE PERIODS AND FREQUENCIES ########################################
f_0 = f_q/q; T_0 = 1/f_0#-------------- quasiperiod T_0 = 1/f_0
f_p= p*f_0#---------------------------- Defines p-wave pitch=f_p
if dfstr=="q":
Df_p=0; Df_q = f_b/p/beatFactor
else:
Df_q=0; Df_p = f_b/q/beatFactor;
cents=abs(1200*(np.log2(1+Df_p/f_p)-np.log2(1+Df_q/f_q)))#- error in cents
f_b=abs(p*Df_q - q*Df_p)*beatFactor#----------------------- f_b calculated
T_b=1/f_b; tmax=Nbeats*T_b#-------------------------------- also T_b, tmax
datapoints=int(round(tmax/dt))#--------- datapoints in passage is integer
om_p,om_q=(f_p+Df_p)*2*PI,(f_q+Df_q)*2*PI#-- omega_p, omega_q defined
## CREATE ONE CLICK ######################################################
om_c=10000#--pitch of click
clickcycles=om_c*clickT/2/PI#---guess number click cycles per click
drint("\nclickT,clickcycles= %.5e , %.1f (initial)"%(clickT,clickcycles))
clickcycles=round(clickcycles)#-actual number click cycles
clickT=2*PI*clickcycles/om_c#---actual (adjusted) click time
drint("clickT,clickcycles= %.5e , %.1f (adjusted)"%(clickT,clickcycles))
clickpoints=round(clickT/dt)#---actual (adjusted) integer click length
drint("number datapoints per click %i (adjusted)"%clickpoints)
## Single click, Duration = (modified) clickT: ###########################
clickt=np.linspace(0,clickT,num=clickpoints)#-len(clickt)<<len(t)
cy1=clickA*Amp*np.sin(om_c*clickt)#-----------much smaller array w/ clickt
cy2=clickA*Amp*np.sin(1.5*om_c*clickt)#-------cy2 is the high frequency click
#plt.scatter(clickt,cy2,s=2); plt.show()
## Declare 4 numpy arrays: t, yp, yq, yc #################################
t =np.linspace(0, tmax, num=datapoints)#-
yp=Amp*np.cos( om_p*t )#------------------ yp (numpy array for high pitch)
yq=Amp*np.cos( om_q*t)#------------------- yq (numpy array for low pitch)
yc=np.zeros(datapoints)#-------------------yc (numpy array for clicks)
## Three units of time: seconds, beats, samples.
## t is measured in seconds
## B is measured in beats: B=t/T_b
## X is measured in samples: X=t*samplerate
B=0; Blist=[]; tlist=[]; Xlist=[]
j=0#------------------------ Create 3 lists of clicktimes (1 for each time unit)
for i in range(len(clickmap)):
if not ifclick[i]:#- IF this section has no clicks:
B+=clickmap[i]# ...advance B but do nothing
else:#-------------- ELSE enter B values into Blist
for j in range(clickmap[i]):
Blist.append(B)#------counting beats
tlist.append(B*T_b)#--measuring t=time in seconds
Xlist.append(int(round(B*T_b*samplerate)))
#Xlist measures time in "datapoints" of numpy arrays
B+=1#-------Hops ahead in time as per clickmap "instructions"
# print(i,j)#temp
drint('Blist=%s, tlist=%s, Xlist=%s'%(Blist, tlist, Xlist))
##clickmap=[2, 3, 3, 1, 1] -> Blist=[2, 3, 4, 8]. Recall that for Blist[i],
## ... no clicks appear at the silences at i=0 and i=2. Hence we have:
## ... three clicks (at 2,3,4) and 1 click at 8 with silence at 5,6,7
## yc currently contain all zeros.
j=0#------------------------ Iterates the first count in a bar
for i in range(len(Xlist)):#-Insert clicks into yc=[0,0,0...]
first=Xlist[i]
last=first+clickpoints #-clickpoints is lengh in X variables
if j%bar ==0:#--------- bar = number of beats per bar
yc[first:last]=cy1
#print("j==0, yc[first+3] ",j, yc[first+3])
else:
#print("j",j)
yc[first:last]=.5*cy2
j+=1
drint("lengths of %i %i %i"%(len(yp),len(yq),len(yc)))
y=yp+yq+yc#--------------------- adding numpy arrays
Ntrim=round(samplerate/(4*f_0))# trim beginning and end of passage
for i in range(Ntrim):
y[i]=(i/Ntrim)*y[i]#-------- i/Ntrim acts as time-dependent amplitude
j=-Ntrim+i#----------------- at i=0, j is Ntrim points away from end
## OUTPUT (all goes into a cleared directory called "interval_output" ########
if not os.path.exists("interval_output"): os.mkdir("interval_output")
for f in os.listdir("interval_output"):
if not f=="readme.txt":
if clearOut:os.remove(os.path.join("interval_output", f))
#------------------------------- output in /image directory
namestring=interval+"-"+str(round(f_p+Df_p,3))+"-"+str(round(f_q+Df_q,3))
drint("*"+namestring)
path2sound=os.path.join("interval_output", namestring+".wav")
sf.write(path2sound, y, samplerate)# sf = soundfile
path2image=os.path.join("interval_output", namestring+".png")
#plt.figure(figsize=(1,10))#-------- plt=variable name of plot
#----------------------------------- Output, image, documentation
drint("%s seconds" % (time.time() - start_time))
drint(f"len(y)={len(y)}")
if makeplot:
plt.figure(figsize=(10,1))#---------------------------Plot figure
plt.scatter(t,y,s=2)#plt.plot(t,y);
plt.axhline(0, lw=1)
plt.xlabel(plotlabel)
plt.savefig(path2image,pad_inches=.1,bbox_inches="tight",dpi=300)
plt.show()#---plot (and show) sound file y=y(t)
drint("\nCHECK Default=Actual?")
drint("*beat frequency f_b=1.50=%s"%f_b)
drint("*beats present Nbeats=10=%s"%Nbeats)
drint("*cents=2.1626911608=%s"%cents)
drint("*datapoints=294000=%s" %datapoints)
drint("*path2image=interval_output\\fifth-300.0-200.25=%s"%path2image)
drint("bar*clipmap=[2, 3, 3, 1, 1]=%s"%clickmap)
drint("0.07697105407714844 seconds=>%s" %(time.time() - start_time))
drint("END")
print(docmate)#--docmate=documentation (string)
with open("interval_output/docmate.txt", "w") as text_file:
text_file.write(docmate)
if makesound: playsound.playsound(path2sound)
return
################## CODE ###########
intervalList=["fifth", "Maj 6th", "fourth", "Maj 3rd",
"min 6th", "min 3rd", "tritone"]
for i in range(7):
print(intervalList[i])
ifun(intervalList[i])