Python/Musical intervals (numpy matplotlib)/playsound

This code uses the module playsound to create the WAV sound files located at:

A musical fifth detuned from just intonation by 2.16 cents

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

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

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])