├── .gitignore ├── demo.gif ├── __pycache__ └── config.cpython-35.pyc ├── requirements.txt ├── config.py ├── plotlib.py ├── README.md ├── _plotter_faustwatch.py └── faustwatch.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__ 3 | .vscode 4 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hrtlacek/faustTools/HEAD/demo.gif -------------------------------------------------------------------------------- /__pycache__/config.cpython-35.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hrtlacek/faustTools/HEAD/__pycache__/config.cpython-35.pyc -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | scipy==1.1.0 2 | numpy==1.15.4 3 | pyinotify==0.9.6 4 | pyo==0.9.0 5 | pyqtgraph==0.10.0 6 | pyinotify==0.9.6 7 | pyzmq==17.1.2 8 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | #----------Python config---------------- 2 | pythonExec = '/root/miniconda2/envs/findRefrain3/bin/python' #python executeable to run async plot process 3 | 4 | #----Temp audio file locations---------- 5 | audioOutPath = '/tmp/offlineOutput.wav' #compiled app writes to this location 6 | audioInPath = '/tmp/offlineInput.wav' #Python writes to this location 7 | 8 | #----------Faust config---------------- 9 | offlineCompArch = '/opt/faust/architecture/sndfile.cpp' #architecture file of compilation -------------------------------------------------------------------------------- /plotlib.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | #title : plotlib.py 4 | #description : plotting library for the faustwatch utility 5 | #author : Patrik Lechner 6 | #date : Jan 2018 7 | #python_version : 3.6.3 8 | #======================================================================= 9 | 10 | import subprocess 11 | import shlex 12 | import sys 13 | import os 14 | import logging 15 | 16 | import zmq 17 | import time 18 | import pickle 19 | import numpy as np 20 | import config 21 | 22 | class Plotter(object): 23 | def __init__(self, *args, randomizePort=True, port=5555): 24 | if randomizePort: 25 | self.port = np.random.randint(1000, 5000) 26 | else: 27 | self.port = port 28 | logging.debug('initializing plotter at port %i'%self.port) 29 | 30 | modDir = os.path.dirname(os.path.abspath(__file__)) 31 | 32 | self.plotterFile = os.path.join(modDir, '_plotter_faustwatch.py') 33 | 34 | self.__createPlotProcess() # create the plotting server/subprocess 35 | time.sleep(0.1) # give the process some time to launch 36 | self.__connect() # connect to the plotting server 37 | 38 | return 39 | 40 | def __connect(self): 41 | self.context = zmq.Context() 42 | return 43 | 44 | def __createPlotProcess(self): 45 | cmd = config.pythonExec+' ' + \ 46 | self.plotterFile+' '+str(self.port) 47 | logging.debug('starting subprocess via: '+ cmd) 48 | cmd = shlex.split(cmd) 49 | self.proc = subprocess.Popen(cmd) 50 | return 51 | 52 | def plot(self, arr): 53 | self.data = arr 54 | self.socket = self.context.socket(zmq.REQ) 55 | self.socket.connect("tcp://127.0.0.1:"+str(self.port)) 56 | 57 | pickled = pickle.dumps(self.data, protocol=0).decode('latin-1') 58 | msg = {'type': 'data', 'data': pickled} 59 | self.socket.send_json(msg) 60 | return 61 | 62 | def addPlot(self): 63 | return 64 | 65 | def destroy(self): 66 | self.proc.kill() 67 | return 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tools for analyzing Faust programs 2 | At the moment there is one tool present, faustwatch.py 3 | 4 | ## Faustwatch 5 | 6 | Faustwatch is a tool that observes a .dsp file used by the dsp language [FAUST](https://faust.grame.fr/). If the file is changed (saved after editing): 7 | - the blockdiagram can be automatically shown in the default browser 8 | - the impulse response can be plotted in the time domain 9 | - the impulse response can be plotted in the frequency domain. 10 | - the time and frequency domain plots of the last saved version are always visible so the current and last saved version can be compared. 11 | - the impulse response is played back via pyo and Jack Audio 12 | 13 | 14 | Basically it is supposed to make FAUST development faster. 15 | 16 | Here you can see it in action: 17 | ![](demo.gif) 18 | 19 | ### Install 20 | #### Dependencies 21 | This has only been tested under Linux and with python 3.6.3. It requires a number of standared libraries such as numpy,scipy: 22 | - numpy 1.15.4 23 | - scipy 1.1.0 24 | 25 | and it requires some more uncommon libraries: 26 | - pyo 0.9.0 27 | - pyqtgraph 0.10.0 28 | - pyinotify 0.9.6 29 | - pyzmq 17.1.2 30 | 31 | This tool has not been tested with other versions of these libraries. Of course there is a good chance it will work just fine with newer versions of these. 32 | #### Configuration 33 | The file ```config.py``` has to be changed in order to make sure that: 34 | - the architecture file for offline processing can be found. 35 | - the right python executable is used to start the plotting process. 36 | 37 | 38 | ### usage 39 | ``` bash 40 | usage: faustwatch [-h] [--svg] [--ir] [--af AF] [--impLen IMPLEN] [--line] N 41 | 42 | Watch a dsp file for changes and take a specific action. 43 | 44 | positional arguments: 45 | N Path to a .dsp file 46 | 47 | optional arguments: 48 | -h, --help show this help message and exit 49 | --svg Make an svg block diagram and open it. 50 | --ir Get impulse response and plot it. 51 | --af AF Send through audio file. 52 | --impLen IMPLEN Length of impulse. Default is unit impulse, so 1. 53 | --line Get response to line from -1 to 1. So input-output 54 | amplitude relationship. Useful for plotting transfer 55 | functions of non-linearities 56 | 57 | ``` -------------------------------------------------------------------------------- /_plotter_faustwatch.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from pyqtgraph.Qt import QtGui, QtCore 4 | import numpy as np 5 | import pyqtgraph as pg 6 | import zmq 7 | import time 8 | import pickle 9 | import codecs 10 | import sys 11 | import scipy.signal as sig 12 | 13 | import logging 14 | 15 | 16 | logging.captureWarnings(True) 17 | 18 | logging.basicConfig(level=logging.CRITICAL) 19 | logging.debug('Logger process started.') 20 | 21 | global plots 22 | plots = [] 23 | 24 | # print (sys.argv) 25 | 26 | try: 27 | port = sys.argv[1] 28 | except IndexError: 29 | port = 5555 30 | 31 | app = QtGui.QApplication([]) 32 | 33 | # =======NETWORK-INIT=============== 34 | context = zmq.Context() 35 | socket = context.socket(zmq.REP) 36 | socket.bind("tcp://*:"+str(port)) 37 | 38 | # ==========Graphics-INIT=========== 39 | 40 | # pg.setConfigOptions(antialias=True) #anti aliasing seems to slow down a lot 41 | win = pg.GraphicsWindow(title="FAUSTwatch") 42 | irPlot = win.addPlot() 43 | irPlot.addLegend() 44 | irPlot.setWindowTitle('pyqtgraph example: Legend') 45 | 46 | c1 = irPlot.plot([1, 2, 3, 4], pen='r', name='last IR', alpha=0.1) 47 | 48 | plots.append(c1) 49 | c2 = irPlot.plot([2, 1, 4, 3], pen='w', fillLevel=0, 50 | fillBrush=(255, 255, 255, 30), name='current IR', alpha = 0.5) 51 | plots.append(c2) 52 | irPlot.showGrid(x=True, y=True) 53 | 54 | win.nextRow() 55 | specPlot = win.addPlot() 56 | specPlot.addLegend() 57 | 58 | specPlot.setLogMode(True, False) 59 | 60 | sp1 = specPlot.plot([0, 1, 2, 3], pen='r', name='last') 61 | sp2 = specPlot.plot([0, 1, 2, 3], pen='w', name='current', fillLevel=-180, 62 | fillBrush=(255, 255, 255, 30)) 63 | specPlots = [sp1,sp2] 64 | irPlot.showGrid(x=True, y=True) 65 | 66 | 67 | 68 | colors = ['r', 'g', 'b', 'y', 'w'] 69 | 70 | 71 | def update(): 72 | 73 | try: 74 | js = socket.recv_json(flags=1) 75 | # print(js) 76 | socket.send(b"ok") 77 | 78 | if js['type'] == 'data': 79 | arr = pickle.loads(js['data'].encode('latin-1')) 80 | nPlots = getNPlots(arr) 81 | currNPlots = len(plots) 82 | diff = nPlots-currNPlots 83 | 84 | for i in range(diff): 85 | plots.append(irPlot.plot( 86 | [0, 1, 2], pen=colors[i % len(colors)])) 87 | if nPlots > 1: 88 | for i in range(nPlots): 89 | thisPlot = plots[i] 90 | thisPlot.setData(arr[:, i]) 91 | 92 | thisSpec = specPlots[i] 93 | spec,f = getSpec(arr[:,i]) 94 | thisSpec.setData(f,spec) 95 | else: 96 | c1.setData(arr) 97 | # try: 98 | # name = js['labels'][0] 99 | # c1.name = name 100 | # # plt.addLegend() 101 | # except: 102 | # pass 103 | # for i in range(nPlots) 104 | # print(arr) 105 | 106 | elif js['type'] == 'cmd': 107 | print('received command:', js['data']) 108 | 109 | except zmq.error.Again: 110 | pass 111 | 112 | return 113 | 114 | 115 | def getNPlots(arr): 116 | try: 117 | nPlots = arr.shape[1] 118 | except: 119 | nPlots = 1 120 | return nPlots 121 | 122 | def getSpec(arr): 123 | f, Pxx_den = sig.welch(arr, 44100, nperseg=1024) 124 | dbspec = aToDb(Pxx_den) 125 | return dbspec,f 126 | 127 | 128 | def aToDb(a): 129 | db = np.clip(20*np.log10(a), -180, 99) 130 | return db 131 | 132 | timer = QtCore.QTimer() 133 | timer.timeout.connect(update) 134 | timer.start(50) 135 | 136 | QtGui.QApplication.instance().exec_() 137 | -------------------------------------------------------------------------------- /faustwatch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | #!/usr/bin/env python 4 | #title : faustwatch.py 5 | #description : utilities for FAUST development 6 | #author : Patrik Lechner 7 | #date : Jan 2018 8 | #version : 1.2.0 9 | #usage : 10 | #notes : 11 | #python_version : 3.6.3 12 | #======================================================================= 13 | from __future__ import print_function 14 | __author__ = "Patrik Lechner " 15 | 16 | import pyinotify 17 | import subprocess,shlex 18 | import os 19 | import numpy as np 20 | from scipy.io import wavfile 21 | import matplotlib 22 | import config 23 | import scipy.signal as sig 24 | 25 | import plotlib as pl 26 | import logging 27 | 28 | import argparse 29 | 30 | from pyo import Server, Sig, SndTable, Trig, Phasor, OscTrig, SfPlayer 31 | 32 | logging.captureWarnings(True) 33 | logging.basicConfig(level=logging.CRITICAL) 34 | 35 | parser = argparse.ArgumentParser(description='Watch a dsp file for changes and take a specific action.') 36 | parser.add_argument('dspFile', metavar='N', type=str, 37 | help='Path to a .dsp file') 38 | parser.add_argument('--svg', dest='svg', action='store_const', 39 | const=True, default=False, 40 | help='Make an svg block diagram and open it.') 41 | 42 | parser.add_argument('--ir', dest='ir', action='store_const', 43 | const=True, default=False, 44 | help='Get impulse response and plot it.') 45 | 46 | # Hotfix: disableBroken: audio file input feature 47 | # parser.add_argument('--af', dest='af', type=str,nargs=1, default='', help='Send through audio file.') 48 | 49 | parser.add_argument('--impLen', type=int, default = 1, help='Length of impulse in samples. Default is unit impulse, so 1.') 50 | 51 | parser.add_argument('--length', type=float, default=1, 52 | help='File Length in seconds. Default 0.5') 53 | 54 | # Hotfix: disableBroken: line feature 55 | # parser.add_argument('--line', dest='line', action='store_const', 56 | # const=True, default=False, 57 | # help='Get response to line from -1 to 1. So input-output amplitude relationship. Useful for plotting transfer functions of non-linearities') 58 | 59 | 60 | args = parser.parse_args() 61 | dspFile = args.dspFile 62 | svg = args.svg 63 | ir = args.ir 64 | impLen = args.impLen 65 | lenSec = float(args.length) 66 | 67 | logging.debug(lenSec) 68 | 69 | # Hotfix: disableBroken: line feature 70 | # line = args.line 71 | line = False 72 | 73 | try: 74 | af = args.af[0] 75 | except: 76 | af = '' 77 | 78 | class bcolors: 79 | HEADER = '\033[95m' 80 | OKBLUE = '\033[94m' 81 | OKGREEN = '\033[92m' 82 | WARNING = '\033[93m' 83 | FAIL = '\033[91m' 84 | ENDC = '\033[0m' 85 | BOLD = '\033[1m' 86 | UNDERLINE = '\033[4m' 87 | 88 | class DspFileHandler(): 89 | def __init__(self, dspFile, svg=False, ir=False, af='', line=False, impLen=1, lenSec = 0.5, plotter=None): 90 | self.svg = svg 91 | self.dspFile = dspFile 92 | self.ir = ir 93 | self.af =af 94 | self.line=line 95 | self.lenSec = lenSec 96 | self.impLen = impLen 97 | self.sr = 44100. 98 | self.lenSamps = int(round(self.lenSec*self.sr)) 99 | self.audioInitialized = False 100 | self.irAvailable = False 101 | 102 | logging.debug(self.lenSamps) 103 | 104 | self.lastIR = np.zeros(self.lenSamps) 105 | self.lastSpec = None 106 | self.lastLine = None 107 | 108 | self.dspDir = os.path.dirname(os.path.abspath(dspFile)) 109 | self.baseName = os.path.basename(dspFile) 110 | self.projectName = self.baseName[:-4] 111 | self.outputPath= config.audioOutPath 112 | self.inputPath= config.audioInPath 113 | self.plotter = plotter 114 | 115 | logging.info('watching file: '+os.path.abspath(dspFile)) 116 | 117 | # self.initializeAudio() 118 | 119 | def initializeAudio(self): 120 | self.audioServer = Server(audio='jack') 121 | self.audioServer.boot() 122 | self.audioServer.start() 123 | self.reloadAudioFile() 124 | self.audioInitialized = True 125 | 126 | 127 | def reloadAudioFile(self): 128 | self.sfplayer = SfPlayer(self.outputPath, loop=False, mul=1).out() 129 | 130 | def compute(self): 131 | if not self.svg and not self.ir and not self.af and not self.line: 132 | logging.info('only compiling, no other action.') 133 | 134 | cmd = 'faust '+self.dspFile 135 | cmd = shlex.split(cmd) 136 | proc = subprocess.Popen( 137 | cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 138 | resp = proc.communicate()[0] 139 | resp = resp.decode("utf-8") 140 | if 'ERROR' in resp: 141 | print(bcolors.FAIL+'>[ER]'+bcolors.ENDC+resp) 142 | elif 'WARNING' in resp: 143 | print(bcolors.WARNING+'>[WA]'+bcolors.ENDC+resp) 144 | else: 145 | print(resp) 146 | print(bcolors.OKGREEN+'>[OK]'+bcolors.ENDC) 147 | 148 | if self.svg: 149 | cmd = 'faust --svg '+self.dspFile 150 | cmd = shlex.split(cmd) 151 | proc = subprocess.Popen(cmd,stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 152 | resp = proc.communicate()[0] 153 | resp = resp.decode("utf-8") 154 | if 'ERROR' in resp: 155 | print (bcolors.FAIL+'>[ER]'+bcolors.ENDC+resp) 156 | elif 'WARNING' in resp: 157 | print (bcolors.WARNING+'>[WA]'+bcolors.ENDC+resp) 158 | self.openSVG() 159 | else: 160 | print(resp) 161 | print (bcolors.OKGREEN+'>[OK]'+bcolors.ENDC) 162 | self.openSVG() 163 | if self.ir: 164 | returnCode = self.compile() 165 | if returnCode <2: 166 | self.getIR() 167 | self.plotSignalQt() 168 | 169 | if self.line: 170 | returnCode = self.compile() 171 | if returnCode <2: 172 | self.getLineResponse() 173 | self.plotSignalQt() 174 | 175 | if len(self.af)>0: 176 | returnCode = self.compile() 177 | if returnCode<2: 178 | self.inputPath = self.af 179 | self.sr, self.inputSignal = wavfile.read(self.af) 180 | self.processFile(self.af) 181 | self.plotSignalQt() 182 | 183 | return 184 | 185 | def compile(self): 186 | self.binaryPath = 'offlineProcessor' 187 | outfileCpp = 'offline.cpp' 188 | cmd = 'faust -a '+config.offlineCompArch+' -o '+outfileCpp+' '+self.dspFile 189 | cmd = shlex.split(cmd) 190 | proc = subprocess.Popen(cmd,stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 191 | resp = proc.communicate()[0] 192 | resp = str(resp) 193 | 194 | cmd = 'g++ -lsndfile '+outfileCpp+' -o '+self.binaryPath 195 | cmd = shlex.split(cmd) 196 | proc = subprocess.Popen(cmd,stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 197 | resp1 = proc.communicate()[0] 198 | if 'ERROR' in resp: 199 | print (bcolors.FAIL+'>[ER]'+bcolors.ENDC+resp) 200 | return 2 201 | elif 'WARNING' in resp: 202 | print (bcolors.WARNING+'>[WA]'+bcolors.ENDC+resp) 203 | return 1 204 | 205 | else: 206 | print (bcolors.OKGREEN+'>[OK]'+bcolors.ENDC) 207 | return 0 208 | 209 | def openSVG(self): 210 | 211 | svgPath = os.path.join(self.dspDir,self.projectName+'-svg','process.svg') 212 | cmd = 'xdg-open '+svgPath 213 | cmd = shlex.split(cmd) 214 | proc = subprocess.Popen(cmd,stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 215 | 216 | def getIR(self): 217 | impOffsetSamps = int(round(self.lenSamps*0.25)) 218 | impLength = self.impLen 219 | imp = np.zeros(self.lenSamps) 220 | imp[impOffsetSamps:impOffsetSamps+impLength] = 1 221 | self.processArray(imp) 222 | 223 | return 224 | 225 | def getLineResponse(self): 226 | line = np.linspace(-1,1,self.sr) 227 | self.processArray(line) 228 | return 229 | 230 | def processFile(self, tempPath): 231 | 232 | cmd = os.path.join(self.dspDir,self.binaryPath)+' '+tempPath+' '+self.outputPath 233 | cmd = shlex.split(cmd) 234 | proc = subprocess.Popen(cmd,stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 235 | resp = proc.communicate()[0] 236 | self.irAvailable = True 237 | if not self.audioInitialized: 238 | self.initializeAudio() 239 | self.play() 240 | return 241 | 242 | def processArray(self,anArray, sr=44100,inputPath='/tmp/offlineInput.wav'): 243 | assert type(anArray) ==np.ndarray 244 | self.inputSignal = anArray.astype(np.float32) 245 | wavfile.write(inputPath, sr, anArray) 246 | self.inputPath = inputPath 247 | 248 | cmd = os.path.join(self.dspDir,self.binaryPath)+' '+inputPath+' '+self.outputPath 249 | cmd = shlex.split(cmd) 250 | proc = subprocess.Popen(cmd,stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 251 | resp = proc.communicate()[0] 252 | 253 | if not self.audioInitialized: 254 | self.initializeAudio() 255 | self.play() 256 | 257 | return 258 | 259 | def play(self): 260 | logging.debug('play function called') 261 | self.reloadAudioFile() 262 | # self.trig.play() 263 | 264 | 265 | 266 | def plotSignalQt(self): 267 | _, y = wavfile.read(self.outputPath) 268 | currentAndLast = np.array([self.lastIR,y]).T 269 | 270 | self.plotter.plot(currentAndLast) 271 | self.lastIR = y 272 | 273 | return 274 | 275 | def getSpec(self): 276 | x = self.inputSignal 277 | f, Pxx_den = sig.welch(x, self.sr, nperseg=1024) 278 | 279 | global MyDspHandler 280 | 281 | if ir: 282 | plotter = pl.Plotter() 283 | MyDspHandler = DspFileHandler( 284 | dspFile, svg=svg, ir=ir, af=af, line=line, impLen=impLen, plotter=plotter, lenSec=lenSec) 285 | else: 286 | MyDspHandler = DspFileHandler( 287 | dspFile, svg=svg, ir=ir, af=af, line=line, impLen=impLen, plotter=None, lenSec=lenSec) 288 | 289 | class EventHandler(pyinotify.ProcessEvent): 290 | def process_IN_CREATE(self, event): 291 | print ("Creating:", event.pathname) 292 | 293 | def process_IN_DELETE(self, event): 294 | print ("Removing:", event.pathname) 295 | 296 | def process_IN_CLOSE_WRITE(self,event): 297 | MyDspHandler.compute() 298 | 299 | # Instanciate a new WatchManager (will be used to store watches). 300 | wm = pyinotify.WatchManager() 301 | # Associate this WatchManager with a Notifier (will be used to report and 302 | # process events). 303 | handler = EventHandler() 304 | notifier = pyinotify.Notifier(wm,handler) 305 | # Add a new watch on /tmp for ALL_EVENTS. 306 | wm.add_watch(dspFile, pyinotify.ALL_EVENTS) 307 | # Loop forever and handle events. 308 | notifier.loop() 309 | 310 | 311 | --------------------------------------------------------------------------------