{
   "description" : "Calculates and plots the spectrum at any position in a sound file (only reads one channel).\r\nDisplays the spectrogram of a selected segment of a sound file.\r\nBe careful not to select a very long segment or it will take very long to calculate and display (anything under 3 seconds is ok).",
   "ancestor_list" : [],
   "name" : "Spectrum and Spectrogram of a sound file",
   "author" : "jcc",
   "id" : "1-4WU",
   "is_private" : null,
   "code" : "//\r\n// Spectrum and Spetrogram of a sound\r\n//\r\n// Based on the material taught in: Audio Signal Processing for Music Applications\r\n// by Prof Xavier Serra, Prof Julius O Smith, III\r\n// https://www.coursera.org/course/audio\r\n//\r\n// jcc\r\n//\r\n// Select all code and evaluate\r\n\r\n(\r\ns.waitForBoot({\r\n// vars for fft\r\nvar sfBuf, soundFile, sfPath, sfview, spectrum, win, selectionSpec, openButton, sfselDisp,\r\nzoButton,ziButton,srButton,slButton,cursorFrame, sfDispArray, sfDispButton,loadDisp, hwin,real,imag,cosTable,fftsize,makefft,fftDisp, makefftB, sfSampleRate ;\r\n\r\n//vars for spetrogram\r\nvar sgrmBuf,sonogramButton, sfSelection, windowsize, overlap, winStepArray, stepsNum;\r\nvar sfSliceArray, shwin, sreal, simag, sCosTable, sonSpectrum, spectrumArray;\r\nvar minmax, minSpectrumAmp,maxSpectrumAmp ,spctrgrm, cspctrgrm;\r\nvar readSlice, analyzeSlice, getMinMaxVals, makeSonogram, displaySonogram,clrspctrgrm; //function names\r\n\r\n// init fft vars\r\nfftsize= 2**11;\r\nsfSampleRate = 44100; //temporary sr\r\nsfDispArray=FloatArray.newClear(512);\r\nhwin = Signal.hanningWindow(512);\r\nreal = Signal.newClear(fftsize);\r\nimag = Signal.newClear(fftsize);\r\ncosTable = Signal.fftCosTable(fftsize);\r\n//\"sfSampleRate; \".post;sfSampleRate.postln;\r\n\r\n//init sonogram vars\r\nwindowsize = 512;\r\noverlap= windowsize/2;\r\n//init the next 3 variables when making a selection\r\n//winStepArray=Array.series((~sfBuf.numFrames - windowsize).div(overlap), 0,  ~overlap);\r\n//stepsNum=winStepArray.size;\r\n//spectrumArray=DoubleArray.newClear(fftsize/2)!(stepsNum);\r\nsfSliceArray=DoubleArray.newClear(512);\r\nshwin = Signal.hanningWindow(512);\r\nsreal = Signal.newClear(fftsize);\r\nsimag = Signal.newClear(fftsize);\r\nsCosTable = Signal.fftCosTable(fftsize);\r\n\r\n// fft functions\r\nloadDisp={arg f;\r\n\tsfBuf.loadToFloatArray( f, 512,{arg a; sfDispArray = a});\r\n\tsfDispArray = sfDispArray;\r\n\t//\tsfselDisp.refresh\r\n};\r\nmakefft = {arg signalToAnalize;\r\n\thwin = Signal.hanningWindow(512);\r\n\tsignalToAnalize = signalToAnalize * hwin;  // window signal\r\n\treal=real.waveFill({arg x; // extend signal for higher FFT definition\r\n\t\tif((signalToAnalize.size) < x,\r\n\t\t\t{0},\r\n\t\t\t{signalToAnalize[x]})}, 0, fftsize-1);\r\n\tspectrum = fft(real, imag, cosTable) // do FFT\r\n};\r\n~loadFFT=Routine({\r\n\r\n\t// display 512 sound file samples from cursor\r\n\t// bug somewhere here, always requires two mouse clicks to work...\r\n\t// why??\r\n\r\n\tsfselDisp.value = loadDisp.value(cursorFrame);\r\n\t0.0001.wait;\r\n\tsfselDisp.refresh;\r\n\t// make fft and plot spectrum\r\n\tfftDisp.value = makefft.value(sfDispArray).magnitude[0..1024]; //plot only positive frecuencies\r\n\tfftDisp.domainSpecs = [0, (sfSampleRate / 2), \\lin,0,0,\"Hz\"].asSpec;\r\n\tfftDisp.refresh;\r\n});\r\n\r\n// sonogram functions\r\n\r\n(readSlice={arg f,n;\r\n\tsgrmBuf.loadToFloatArray( f, 512,{arg a; sfSliceArray = a});\r\n\t sfSliceArray = sfSliceArray;\r\n\t\"Reading spectrum slice number: \".post;(n+1).postln;\r\n});\r\n\r\n(\r\nanalyzeSlice = {arg signalToAnalize;\r\n\tsignalToAnalize = signalToAnalize * shwin;  // window signal\r\n\tsreal=sreal.waveFill({arg x; // extend signal for higher FFT definition\r\n\t\tif((signalToAnalize.size) < x,\r\n\t\t\t{0},\r\n\t\t\t{signalToAnalize[x]})}, 0, fftsize-1);\r\n\tfft(sreal, simag, sCosTable) ;// do FFT\r\n});\r\n\r\n(\r\ngetMinMaxVals={\r\nminmax=spectrumArray.size.collectAs({|i,n|\r\n\t[spectrumArray[i][spectrumArray[i].minIndex], spectrumArray[i][spectrumArray[i].maxIndex]];\r\n},Array).flop;\r\nminSpectrumAmp=minmax[0][minmax[0].minIndex];\r\n\"Min amp value :\".post;minSpectrumAmp.postln;\r\nmaxSpectrumAmp=minmax[1][minmax[1].maxIndex];\r\n\"Max amp value :\".post;maxSpectrumAmp.postln;\r\n});\r\n\r\n(\r\ndisplaySonogram={\r\nvar nx= spectrumArray.size, ny= spectrumArray[0].size,\r\n\txs = 1200, ys = 500, dx= xs/nx, dy=ys/ny, spctrgrm, stext, ttext, ftext,bcolor;\r\n\r\n\tgetMinMaxVals.value;\r\n\r\n\tbcolor= Color.new(0.63915638923645, 0.61455166339874, 0.3189784526825);\r\n\tstext= StaticText(win, Rect(30, 900, 250, 20)).background_(bcolor);\r\n\tttext = StaticText(win, Rect(280, 900, 250, 20)).background_(bcolor);\r\n\tftext = StaticText(win, Rect(530, 900, 250, 20)).background_(bcolor);\r\n\tstext.string = \" Sample range: \";\r\n\tttext.string = \" Time range: \";\r\n\tftext.string = \" Bin freq: \";\r\n\tspctrgrm = UserView(win, Rect(30, 400, xs, ys));\r\n\tspctrgrm.background = Color.black;\r\n\tspctrgrm.drawFunc = {\r\n\t\tnx.do{ |x|\r\n\t\t\tny.do{ |y|\r\n\t\t\t\tvar m = spectrumArray[x][y];\r\n\t\t\t\tPen.color = Color.green(m.curvelin(minSpectrumAmp,maxSpectrumAmp,0,2,-1), 0.95);\r\n\t\t\t\tPen.addRect(\r\n\t\t\t\t\tRect(x*dx, ys-(y * dy), dx, dy)\r\n\t\t\t\t);\r\n\t\t\t\tPen.fill\r\n\t\t\t}\r\n\t\t}\r\n\t};\r\n\tspctrgrm.mouseDownAction = {arg v, x, y;\r\n\t\tvar binf, nextbinf, sampleNum;\r\n\t\tsampleNum =winStepArray[x.linlin(0, xs, 0, stepsNum)];\r\n\t\tsampleNum= sampleNum + sfSelection[0];\r\n\t\t//\"Yval: \".post;y.postln;\r\n\t\tbinf =((ys-y)/ys*2048)*sfSampleRate/4096;\r\n\t\tnextbinf = ((ys-y + 1)/ys*2048)*sfSampleRate/4096;\r\n\t\tstext.string = \" Sample range: \"++sampleNum++\" - \"++(sampleNum+512);\r\n\t\tttext.string = \" Time range: \"++(sampleNum/sfSampleRate).round(0.001)++\" - \"\r\n\t\t++((sampleNum+512)/sfSampleRate).round(0.001)++\" secs\";\r\n\t\tftext.string = \" Bin freq: \"++binf.round(0.001)++\" - \"++nextbinf.round(0.001)++\" Hz\";\r\n\t};\r\n\tspctrgrm.mouseMoveAction = spctrgrm.mouseDownAction;\r\n}\r\n);\r\n\r\n(\r\n~loadSpectrogram= Routine({\r\n\tvar half = Array.series(fftsize /2);\r\n\tclrspctrgrm.value;\r\n\tspectrumArray.size.do{|n|\r\n\t\treadSlice.value(winStepArray[n],n);\r\n\t\t0.0001.wait;\r\n\t\tspectrumArray.put(n, analyzeSlice.value(sfSliceArray).abs.at(half).abs);\r\n\t\t0.0001.wait;\r\n\t};\r\n\t\"Done\".postln;\r\n\r\n\tdisplaySonogram.value;\r\n})\r\n);\r\n\r\nBuffer.freeAll;\r\nWindow.closeAll;\r\n\r\n////////////\r\n//\r\n// GUI\r\n//\r\n///////////\r\n\r\n//Window\r\nwin = Window(\"Spectrum and Spectrogram\", Rect(20, 4000, 1260, 960), false).front;\r\nwin.view.background_(Color.new(0.52692360877991, 0.46053557395935, 0.30619597434998));\r\nwin.alpha_(0.98);\r\nwin.onClose = {Buffer.freeAll; \"Adiós\".postln;\"\".postln};\r\n//Open file to analyze\r\nopenButton = Button.new(win, Rect(30, 0, 130, 20))\r\n.states_([[\"Select sound file\", Color.black, Color.new(0.63915638923645, 0.61455166339874, 0.3189784526825)]])\r\n.action_({\r\n\tDialog.openPanel(\r\n\t\tokFunc: { |path|\r\n\t\t\tsfPath = path;\r\n\t\t\tsoundFile = SoundFile.new;\r\n\t\t\tsoundFile.openRead(path);\r\n\t\t\tsfSampleRate = soundFile.sampleRate;\r\n\t\t\t\"sfSampleRate; \".post;sfSampleRate.postln;\r\n\t\t\tsfBuf = Buffer.readChannel(s, path, channels:0);\r\n\t\t\tsfview.soundfile_(soundFile);\r\n\t\t\tsfview.read(0, soundFile.numFrames);\r\n\t\t\tsfview.timeCursorOn = true;\r\n\t\t\tsfview.timeCursorPosition = 0;\r\n\t\t\tcursorFrame = 0;\r\n\t\t\tsfview.gridResolution = 1;\r\n\t\t\tsfview.mouseUpAction.value(sfview); // why this?\r\n\t\t\t//sfselDisp.value = loadDisp.value(cursorFrame);\r\n\t\t\tsfview.setSelectionStart(0,0);\r\n\t\t\tsfview.setSelectionSize(0,0)\r\n\r\n\t\t},\r\n\t\tcancelFunc: {\"cancelled\"}\r\n\t);\r\n});\r\n//sfView\r\nsfview = SoundFileView.new(win, Rect(30, 20, 1200, 100));\r\nsfview.mouseUpAction = {arg view;\r\n\tcursorFrame = sfview.timeCursorPosition;\r\n\t\"Cursor at frame: \".post; cursorFrame.postln; \"\".postln;\r\n\tsfSelection = sfview.selections[sfview.currentSelection];\r\n\t\"selection start, size: \".post; sfSelection.postln;\r\n\t\"selection duration: \".post; (sfSelection[1] / sfSampleRate).postln;\"\".postln;\r\n\tsgrmBuf = Buffer.readChannel(s, sfPath, sfSelection[0], sfSelection[1], 0);\r\n\twinStepArray=Array.series((sfSelection[1] - windowsize).div(overlap), 0,  overlap);\r\n\tstepsNum=winStepArray.size;\r\n\tspectrumArray=DoubleArray.newClear(fftsize/2)!(stepsNum);\r\n\r\n};\r\n\r\n\r\n\r\n\r\n\r\n// zoom and scroll buttons\r\nzoButton= Button.new(win, Rect(30, 120, 50, 30))\r\n.states_([[\"-\"]])\r\n.action_({sfview.zoom(2).refresh});\r\nziButton= Button.new(win, Rect(80, 120, 50, 30))\r\n.states_([[\"+\"]])\r\n.action_({sfview.zoom(0.75).refresh});\r\nslButton= Button.new(win, Rect(150, 120, 50, 30))\r\n.states_([[\"<-\"]])\r\n.action_({sfview.scroll(-0.1).refresh});\r\nsrButton= Button.new(win, Rect(200, 120, 50, 30))\r\n.states_([[\"->\"]])\r\n.action_({sfview.scroll(0.1).refresh});\r\n// FFT Analysis Button\r\nsfDispButton= Button.new(win, Rect(250, 120, 250, 30))\r\n.states_([[\"Analyze spectrum at cursor\",Color.black, Color.new(0.76396951675415, 0.87935035228729, 0.62494311332703)]])\r\n.action_({\r\n\tAppClock.play(~loadFFT.reset);\r\n});\r\n// Sonogram Analysis Button\r\nsonogramButton= Button.new(win, Rect(500, 120, 250, 30))\r\n.states_([[\"Display Selection Sonogram\",Color.black, Color.new(0.76396951675415, 0.87935035228729, 0.62494311332703)]])\r\n.action_({\r\n\t(sfSelection[1]!=0).if(\r\n\t\t{AppClock.play(~loadSpectrogram.reset)},\r\n\t\t{\"Select a region in the Sound file View for Spectrogram display first!\".postln}\r\n\t);\r\n});\r\n// Play SF selection\r\nsonogramButton= Button.new(win, Rect(750, 120, 250, 30))\r\n.states_([[\"Play Selection\",Color.black, Color.new(0.76396951675415, 0.87935035228729, 0.62494311332703)]])\r\n.action_({\r\n\t(sfSelection[1]!=0).if(\r\n\t\t{{PlayBuf.ar(sfBuf.numChannels, sfBuf,1,1,sfSelection[0])*\r\n\t\t\tEnvGen.ar(\r\n\t\t\t\tEnv.new([0,1,1,0],[0.001,0.998,0.001]*sfSelection[1]/sfSampleRate),\r\n\t\t\t\tdoneAction:2)}!2\r\n\t\t}.play,\r\n\t\t{\"Select a region in the Sound file View for Spectrogram display first!\".postln}\r\n\t);\r\n});\r\n\r\n\r\n// 512 samples of sound file display\r\nsfselDisp = Plotter.new(bounds: Rect(30, 150, 1200, 100),parent: win);\r\nsfselDisp.value=[0];\r\n// FFT display\r\nfftDisp = Plotter.new(bounds: Rect(30, 280, 1200, 100),parent: win);\r\nfftDisp.value = Array.fill(fftsize/2, {0});\r\nfftDisp.domainSpecs = [0, (sfSampleRate / 2), \\lin,0,0,\"Hz\"].asSpec; fftDisp.refresh;\r\n\r\n// init spectrogram clear display\r\nclrspctrgrm.value;\r\n\r\n\"FFT analysis of 512 samples of any sound file\".postln; \"\".postln;\r\n\"1) select a sound file\".postln;\r\n\r\n\"2) Position the cursor at the point in the soundfile you want to analyze and click 'Analyze Spectrum at cursor' button for a display of the spectrum at that instant\".postln;\r\n\r\n\r\n\"3) Select a region in the sound file display and click 'Display selection sonogram' button for a spectrogam of the selection.  Be careful not to select a very long segment or it will take a VERY LONG time to compute and dispay the spectrogram (anything under 3 seconds is ok)\".postln;\r\n\"\".postln;\r\n});\r\n)",
   "labels" : [
      "spectrum",
      "spectrogram"
   ]
}
