0

I use the following code to do some immediate sound processing/analyzing. It works, but really slow (compared to the planned speed). I have added some time markers to find out where the problem is and according to them there shouldn't be any. Typical duration (see below) is <0.01 s for all three computed times but it still takes around a second to complete the loop. Where is the problem?

Edit: Please note, that the time measurement is not the real issue here. To prove that: MyPeaks basically just finds the maximum of pretty short FFT - nothing expensive. And the problem persists even when these routines are commented out.

  • Should I use something different than lambda function to make the cycle?
  • Did I make some mistake when starting and recording the stream?
  • etc.

    import pyaudio
    import struct
    import mute_alsa
    import time
    import numpy as np
    from Tkinter import *
    
    
    def snd_process(k=0):
     if k<1000:
      t0=time.clock()
    
      data = stream.read(CHUNK)
    
      t1=time.clock()
    
      fl=CHUNK
      int_data = struct.unpack("%sh" %str(fl),data)
      ft=np.fft.fft(int_data)
      ft=np.fft.fftshift(ft)
      ft=np.abs(ft)     
    
      t2=time.clock()
    
      pks=MyPeaks(np.log(ft))
    
      freq_out.configure(text=str(pks))
    
      t3=time.clock()
    
      print t1-t0, t2-t1, t3-t2     
    
      master.after(1, lambda: snd_process(k+1))
    
    CHUNK = 8000
    FORMAT = pyaudio.paInt16
    CHANNELS = 1
    RATE = 4000
    
    p = pyaudio.PyAudio()
    
    stream = p.open(format=FORMAT,
                channels=CHANNELS,
                rate=RATE,
                input=True,
                frames_per_buffer=CHUNK)
    
    #Tkinter stuff
    master=Tk()
    button_play=Button(master, command=snd_process, bg="yellow", text="Analyze")
    button_play.grid(row=0, column=0)
    freq_out = Label(master)
    freq_out.grid(row=0, column=1)
    freq_out.configure(text='base')
    mainloop()
    
Victor Pira
  • 1,132
  • 1
  • 10
  • 28
  • 1
    Speed up your python by using C! (or cython) – erip May 03 '16 at 14:33
  • how do you know the time is processing time not the master.after()? When I've tried numpy against raw C it seems to be quite a bit faster. (for dot which isn't surprising) – paddyg May 03 '16 at 14:47
  • @paddyg I don't, but that's not the issue (see the edit). – Victor Pira May 06 '16 at 09:59
  • 1
    if you run a function every millisecond, and you do that 1000 times, the entire processing _must_ take at least a second, plus the time required to do the actual work. – Bryan Oakley May 06 '16 at 16:48

3 Answers3

4

You are scheduling 1000 callback in tk main thread; for every callback you are using 1 ms delay (after()'s first argument). That means the last loop will start around after 1000 ms (1 second) the first one.

Maybe that is way the loop still takes around a second to complete.

So, try to use after_idle(). I don't think you really need to Speeding up the sound processing algorithm because np is already quite efficient.

[EDIT] Surprise!! you are reading from audio channel at every iteration 1 second 8000 bytes in 16 bits format for a 4000 frame rate. You need a second to have it.

Michele d'Amico
  • 22,111
  • 8
  • 69
  • 76
  • Thanks, but - sadly - it does not work. `after_idle` takes about the same time and when 0 ms timeout is used, it does not update the fields in Tkinter (`freq_out.configure(text=str(pks))`) – Victor Pira May 06 '16 at 13:42
  • So maybe is better do it in packages of 20 or 50 I don't think you need 1 ms UI refresh granularity... – Michele d'Amico May 06 '16 at 13:48
  • @VictorPira Just a note: if your complete cycle without UI take (for instance) 200 ms you are asking a UI refresh of 5000 frame per second: maybe TclTk is not designed for this kind of tasks. – Michele d'Amico May 06 '16 at 14:19
  • 200 ms is 5 frames/second. That should be doable. But you are right that forcing a redraw after 1 ms is probably too much. – Roland Smith May 06 '16 at 14:40
  • @RolandSmith In 200 ms he send 1000 updates -> 1 frame in 0.2 ms -> 5000 frame per second. – Michele d'Amico May 06 '16 at 14:43
  • Ever call to `snd_process` (which contains a single `configure` call) is at least 1 ms apart. So the complete cycle *cannot* finish in 200 ms. – Roland Smith May 06 '16 at 14:49
  • Oh, that was really stupid... But believe me guys that I have tried all of the suggestions. – Victor Pira May 06 '16 at 15:49
  • using zero with `after` isn't a very good idea, as it will possibly starve the event queue, preventing tkinter from handling other events. – Bryan Oakley May 06 '16 at 16:45
  • @BryanOakley Thank I'm not a Tk expert ... I don't use it at all :) – Michele d'Amico May 06 '16 at 16:47
2

Squeezing I/O and calculations into the main loop like you are doing is the classical solution. But there are alternatives.

  1. Do the audio gathering and calculations in a second thread. Since both I/O and numpy should release the GIL, it might be a good alternative here. There is a caveat here. Since GUI toolkits like TKinter are generally not multithread-safe, you should not make Tkinter calls from the second thread. But you could set up a function that is called with after to check the progress of the calculation and update the UI say every 100 ms.

  2. Do the audio gathering and calculations in a different multiprocessing.Process. This makes it completely separate from your GUI. You will have to set up a communication channel like e.g. a Queue to send the pks back to the main process. You should use an after function to check if the Queue has data available and to update the display if so.

Roland Smith
  • 42,427
  • 3
  • 64
  • 94
-1

Depending on the OS you're running at you might nog be measuring actual 'wall-clock' time. See here http://pythoncentral.io/measure-time-in-python-time-time-vs-time-clock/ for some details. Note that for python 3.3 time.clock is deprecated and time.process_time() or time.perf_counter() is recommended.

Ton Plooij
  • 2,583
  • 12
  • 15