FFT Analyser Path - Need help drawing the magnitude to height
-
I am struggling to draw the magnitude/peak/rms of an analyzer to my path. My example snippet is just grabbing the analyser buffer from my scriptnode network and copying it to fill a path in a panel.
After some back and forth with Claude my code has become a bit messy but still I haven't solved scaling the magnitude response of the FFT analyser to the vertical axis. You can see in the example that the signal level can take an effect on the alpha. Does anyone have an example snippet on how to do this?
HiseSnippet 2775.3oc0YstaabbEdojVkHk3hj.Wj9yIF8GqbroHks7En3FIQQYITcgPTUoAtFBC2cH4TsbmE6NTRzFBn+r+q8gouH8MnnOA8Mn8LWVtyPtTRlw1IkvvZ2Yly47cNy41LaiDlOIMkk3TZwiGDSbJ84tMGDw6VqKlF4r6VNkVvscadCLuqylChwooj.mRkl8khoKsvbNxe+mueSbHNxmjOjiyILpOYOZOJOezFq+6oggaiCHGS6Yr5Gu9t9rnZrPVe.Jy5VwIF6eFtC4.rXYy35rCNsqSo661ppeExiZ4iwsvOm7rmsZqG+rm9bLoZP0mt5Sd9pUddqmr5SehuSo4qGP4rjlbLmj5TZtMYACZ1kcQjR.mPSosBIhWp5zDjrZ3sYgABUTLpSstzvfFY1nTGmRtMxsXyprX20ceZ.c334VtuPNAJmBSCXoYrg2rVvq5jfWAPpjAjlSAouzsoeBMlmOi.Oel6tQbRRaLrOYBE0Zcl4e9Ut0XvJh3k6gOircB7xPJ7dbkJO.sRkJKs1hnEifskTXKhf1mFQ6gC2tMeiHb3fTRxhucQD7a4kQ0XIDjOqWLKBXZpbXXaNkiNGmfRg8ZfAu.Ic3J2gv2hlFGhGrY+1sIIMkS6cu1WdOgHsHE7HE9OI.w0i5PiHk8SHfcTN3gs9yDet2XDEGEdJ.SflLsDDYsLz4cO87UxDWlVnQEpkDVxQ0Ksb.liKqFGXqRgFSQ7pnYXATUNkv2vmSOm3wS5SJZgwPnmAlUJpHdzaDbp2roQczP0zfKFA3hBQZl3UDf.zeDAGng9RkCIQc3c0hJgzAEhS4MXTX+D32qd8ZKluaG0l1oeBlSYQJwhs20Odmip2bmC2aKfxpkqrVF3Uy1b2WdvF6c5V0qswOBKnR4pqYO8d0OnV8SMYRkxUprp0p1Z2lM1aie7zM28flvBVs5JVSuS8ce4NGeZyZarW8gfvxLw4.3mjKxlxoqNpGhLFV3iQwPHa5PSUJsCDSrG4bRnBrZrjQmhcHebXXKHcGpMKAQh.d.6gKGPSUOg3cIns29XIUzHXHBpc+HeoYlEowj.vIrPuggaO..TH3RIISERJYPajmbBzK.8eogiK9kEVUF14RTO58nLuWwORXJYRTvhUDnW9U4ZpxnJb00frlVg8FE8iXW2Mhxo3P5aHY1.T5fTNoWglB5vU6MpRm4nCPnAjcleDqOGn0SVYK+cCMseLDTP.I5YLXt1RTOLTQFt7BWcAVyqrTz8APfhEHAknfhzWHSoiwQjvB04QT.uNip4gLv6B0h0OJPDux6RSE9z6IFdS4ndlavpkeArREIuZkWO5jcym7QiMoNYkTLCydkungOzQ4MHJ26o9SZ4Kn7taDF2E6U4xs0+dfYLzRF.sS41PiDxzfBQ7.zqfpSv+t3AntutPa7VDNTVHUyPTnLprcBqmNaIRf2BMxARJalCDuhsx8vc.Wv9AhZZBdJLz6mMlG.N4fVITyINE9Ox1Xenk.QdKHWw3VM67I6C5MTn9Rub4deS1XY6foLyuZH9hXJMxCRMZa7K.OIDd+jHyUUjgeGbT.jUDVFn5PAePAOmjL.z6KzjVnUuqjrlJhzaxhc2Qs8h.EOpH+5ZHJ56LpOoM0vve62ZmnKeMuh9ZfztKuRApmPlkEf5XlmPxKuhgQ3VOYkBmzOjkRZ1ukYg7hrs4wO1F0CXI8j45RybekI0SQbFpMMJPl6.bNn85Wb1xnLFnqzCtL8gTBznzI4be4IXcYLwOn3rnxGviPfWQO7MjDlBCiqKitEkKqB1aDEnjdg3VodBcC1hVB86z.vdsheCA1XDMYqphlhrq0jcFkZj5ERnIxLgXwb3rLugDfhkdNEZUUMVsMMD5ZlDn1bu9LxYkl9fjTt3ryStUxgtmxRVGPtHyCcLm3Q3b9NpYNDylwlT1ugOTPV1BVkv6XrHbz27Bzn9u1Fc6fdciqVMx7t3hdioPtpXfalp86FugVagncUmXZvItWLLf3ZBwW6mPP5HVUkmEKkJC.z69grNdUQeKhtDZY6QJDD4L5RQHQFytO5B60T.ACUxfSTc0Ndd..BlA71zOPs2gdHxaTVceHr59VGYXoqANCLKICwKCiBfMrAKccTZkwa.fDKeKH22KPSvG4si4VZV44RgfW6F8bGXujqrdarN9uQQZC9Iw6qtwxsF7I2m4gUe8ubpAK6ZW04uQE.nfHTin0.YIDtnq+BqTXb.iIbXEySk6yhGXbrbwnlZSA2WPQUhFmhDh7TGdE1wrwwuHhN1ft5i5nUl7iZs1hWYbZzCXbxgQfN81EWXQfciNU61ENm97eghyOVvz5SVNYB8h52qEIY3Yd0Kzozb12x172taYyWUgzXgrHg03vXh98q+ZBczkXE2NmFgvR4x6l6WouaN0IecnAhqXUeLXGI7MuDUm+vtaA6pYLB3IHmXRBmJTmRaQNm5STW52BtaQROCNGNHpg2YAnxJwdmrqDTbVRoTWzM6FubtzTjCLe4BZ.uqSI2R+8RNNcIzNc4h29GNRkbTgbs51DDxeo85CERCGSgb2QDxmXqI6gao0jO0U9rkHNd8hEwSJMpH3jKgmumaSQOO4Qspt9Dupt+huwwY7KZ8Kb2mEzODysu2Wwsiqm.bSstrUwEpFA02FXd64uCWFbkq8xfusP7KcaP49cKFiyT.FAGvODXTeE52wsNjWymmCv4b29ONs2Wdka+8k6pj+hZWJPlJg29RKgeP+dVWoapv2AbjbzWcdo+padRoXQR0DnrSCX8do3dwgji.PCc.CsbbVSHq43YxhUXZSwJ77gl9fnzzoIg27um9rBNpqTpCzXRBEhqbAaf5J4qoQmvFLiHBU8dEw6BGkljn.4K+W3mdxpCMXvjUylLOMoyAD9Erjyj6O5mg.dwlgq36O4rQXH6BQt.p1eE1Uji0fENHtKKh5KFRshLHtQO3HQ7LbtCN8XLMT3f2reJjIO3vHce0BROA1VkL9Sbgi3VFzfYNfE.SMu55TFH+FXPjin3.3lJpJOzcUARKWF4GtRMtfOYeaKkMBvYOYNsYbgM.iT5J2SwaPv2LBV+4t6ldhXVnyBmST0G.8QvSk0RARwRmWHMn1jIduiKV8cYJWLB0z7t.QYN4PxkYElFIzXzQD5QT.4xLFWGbbCfhmRxy7DZfS.3vIpDDlurIicVOrz0XpJFZlt4qUS9Yt+.9bh7yNI2890x2gC.0C8RBrwJxUV88wm6L9V+4NOzmCP33DbTJb7GRUSN2jzidLTCL0ZzQnXkBovZzsf1YirYsZHqUILEaClhpl5a1fFq7tqCsRXwL3cKNsOMqdL5O8292eecwW1fzj.Zdvgo9fgQXmMkRi9voM9AQkZC99uLFdEyg2AmD.6g9VEol859nuSL8mc8Zi0WP4i4+P8Efm61UQ6F577W98nXU.dgLL1jJxoTO5bRHDRKw3WAwvsw8C4YiZGStOKhkk4O2C3HBTupSGhkiUgJzFbN1+LSG5iHgDbpQb5uc88fD83j8k4XmJaw69Guuv8qeiqBtHQhDzOk8sOptYWaelydq5y78Ndm9dN+HGgB4O++mc5u1ExouBRtci94e+9iQ2qeLjQOreB6TeUS+hDFepbDPuijsusf69h2QEb0A8fROm56aypwHbkokvGMsD93okvUmVBexzR3SmVBe1MSnnyfM5yY8TwIP6RMpqZItjpSIYHiy+iaEPMY
Content.makeFrontInterface(400, 200); namespace MinimalFftAnalyser { // Core components const var source = Synth.getDisplayBufferSource("fx"); const var fftTimer = Engine.createTimerObject(); const var pnl_Fft = Content.getComponent("pnl_Fft0"); // Display buffer pnl_Fft.data.buffer = source.getDisplayBuffer(0); pnl_Fft.data.buffer.setActive(true); pnl_Fft.data.path = Content.createPath(); // Processing buffers const buff = Buffer.create(pnl_Fft.data.buffer.getReadBuffer().length); reg lastPoints = []; // Configuration constants const THRESHOLD = 1.0; const SIGNAL_DECAY = 0.1; const SILENCE_THRESHOLD = 0.005; const DISPLAY_BINS = 512; const HEIGHT_SCALE = 1.0; const button = Content.getComponent("Button1"); // State variables reg signalLevel = 0.0; // Button callback for enabling/disabling the FFT inline function onButton1Control(component, value) { if (value == 1) fftTimer.startTimer(30); else fftTimer.stopTimer(); } button.setControlCallback(onButton1Control); // Initialize the FFT system inline function initialize() { pnl_Fft.setPaintRoutine(fftPaintRoutine); updateFFT(); fftTimer.setTimerCallback(updateFFT); fftTimer.startTimer(30); } // Main paint routine for the FFT panel inline function fftPaintRoutine(g) { local bounds = this.getLocalBounds(0); local w = bounds[2]; local h = bounds[3]; local path = this.data.path; g.setColour(Colours.withAlpha(0xFFFFFFFF, signalLevel)); g.fillPath(path, [0, 0, w, h]); } // Detects signal level from buffer data inline function detectSignalLevel() { local magnitude = buff.getMagnitude(0, buff.length); local scaleFactor = 5.0; signalLevel = Math.max(magnitude * scaleFactor, signalLevel * SIGNAL_DECAY); signalLevel = Math.min(1.0, signalLevel); return signalLevel; } // Handles silence or very low signal inline function handleSilence(path, w, h) { for (i = 0; i < lastPoints.length; i++) lastPoints[i] = h/2; path.lineTo(w, h/2); path.lineTo(w, h/2); path.lineTo(0, h/2); path.closeSubPath(); return path; } // Normalizes buffer values to find the maximum inline function normalizeBuffer(actualBins) { local maxVal = 0.000001; // Small non-zero value for (i = 0; i < actualBins; i++) if (Math.abs(buff[i]) > maxVal) maxVal = Math.abs(buff[i]); return maxVal; } // Creates the FFT path with optimized points inline function createFilteredPath() { local bounds = pnl_Fft.getLocalBounds(0); local w = bounds[2]; local h = bounds[3]; local path = Content.createPath(); path.startNewSubPath(0, h/2); local actualBins = Math.min(DISPLAY_BINS, buff.length); detectSignalLevel(); if (lastPoints.length != actualBins) { lastPoints = []; for (i = 0; i < actualBins; i++) lastPoints[i] = h/2; } if (signalLevel < SILENCE_THRESHOLD) return handleSilence(path, w, h); local maxVal = normalizeBuffer(actualBins); for (i = 0; i < actualBins; i++) { local position = Math.log(1 + i) / Math.log(1 + actualBins); local x = position * w; local normalizedValue = Math.abs(buff[i]) / maxVal; local y = h/2 - (normalizedValue * h * HEIGHT_SCALE); y = Math.max(0, Math.min(h, y)); if (Math.abs(y - lastPoints[i]) >= THRESHOLD) { path.lineTo(x, y); lastPoints[i] = y; } else { path.lineTo(x, lastPoints[i]); } } path.lineTo(w, lastPoints[actualBins-1]); path.lineTo(w, h/2); path.lineTo(0, h/2); path.closeSubPath(); return path; } // Main update function called by the timer inline function updateFFT() { pnl_Fft.data.buffer.copyReadBuffer(buff); pnl_Fft.data.path = createFilteredPath(); pnl_Fft.repaint(); } // Initialize everything initialize(); }
-
Just a bump on this if anyone has any suggestions!
@ustk apologies for just throwing you onto this (ignore me if its too forward) but I recall you sharing a solution for mapping the magnitude (or peak value) onto an oscilloscope or path.
-
@HISEnberg I've had a look at your code, is there a particular reason you are building the path manually instead of using the
DisplayBuffer.createPath()
method?mapping the magnitude (or peak value) onto an oscilloscope or path.
I'm not sure I understand. But I might have done weird things lol
-
@ustk Thanks for taking a look! In this particular case I am attempting to create a subpath so that I can ignore painting/drawing any values that are already displayed. I wasn't able to do this with
DisplayBuffer.createPath
, thus the subPath.Long story short is I have one project with a very large FFT display and users are reporting a lot of UI lag, so I suspect this is the culprit. I just cracked the magnitude issue however! This is the current draft of the code (though still a mess from working with AI on this, I am in the process of cleaning it up):
To be honest I don't actually see much performance benefit to this method so far so this might be an effort in vain. The only other real benefits of this script is it will allow you to define the bins (I know you can do this already), but also assign different colour gradients, etc.
Content.makeFrontInterface(400, 400); namespace MinimalFftAnalyser { // Core components const var Analyser1 = Synth.getEffect("Analyser1"); const var source = Synth.getDisplayBufferSource("Analyser1"); const var fftTimer = Engine.createTimerObject(); const var pnl_Fft = Content.getComponent("pnl_Fft0"); // Display buffer pnl_Fft.data.buffer = source.getDisplayBuffer(0); pnl_Fft.data.buffer.setActive(true); pnl_Fft.data.path = Content.createPath(); // Processing buffers const buff = Buffer.create(pnl_Fft.data.buffer.getReadBuffer().length); reg lastPoints = []; // Configuration constants const THRESHOLD = 1.0; const SIGNAL_DECAY = 0.1; const SILENCE_THRESHOLD = 0.05; const DISPLAY_BINS = 4098; // Increase/Decrease for more detail const HEIGHT_SCALE = 0.5; const CURVE = 0.99; // Frequency mapping parameters const MIN_FREQ = 20; // Hz - lowest frequency to display const MAX_FREQ = 20000; // Hz - highest frequency to display const SAMPLE_RATE = Engine.getSampleRate(); // Hz - adjust to match your system Console.print(SAMPLE_RATE); // Amplitude response settings const AMPLITUDE_DECAY = 0.95; // Higher values = slower decay const AMPLITUDE_ATTACK = 0.3; // Higher values = faster attack const MAX_MAGNITUDE = 0.1; // Lowered to make visualization more sensitive const MIN_DISPLAY_DB = -60; // Minimum dB level to display (lower = more sensitive) const button = Content.getComponent("Button1"); // Fixed scaling factor - adjust for desired sensitivity const scaleFactor = 4.0; // Increased for better visibility // State variables reg signalLevel = 0.0; reg smoothedMagnitudes = []; // Array to store smoothed magnitude values // Button callback for enabling/disabling the FFT inline function onButton1Control(component, value) { if (value == 1) fftTimer.startTimer(30); else fftTimer.stopTimer(); } button.setControlCallback(onButton1Control); // Initialize the FFT system inline function initialize() { pnl_Fft.setPaintRoutine(fftPaintRoutine); updateFFT(); fftTimer.setTimerCallback(updateFFT); fftTimer.startTimer(30); } // Main paint routine for the FFT panel inline function fftPaintRoutine(g) { local bounds = this.getLocalBounds(0); local w = bounds[2]; local h = bounds[3]; local path = this.data.path; g.setColour(Colours.withAlpha(0xFFFFFFFF, signalLevel)); g.fillPath(path, [0, 0, w, h]); } // Converts linear magnitude to decibels with a minimum threshold inline function linearToDb(linearValue) { if (linearValue < 0.000001) return MIN_DISPLAY_DB; local dbValue = 20.0 * Math.log10(linearValue); return Math.max(dbValue, MIN_DISPLAY_DB); } // Maps decibels to display height with a better curve inline function dbToDisplayHeight(db, height) { local normalized = (db - MIN_DISPLAY_DB) / (-MIN_DISPLAY_DB); normalized = Math.max(0, Math.min(1, normalized)); normalized = Math.pow(normalized, CURVE); return height/2 - (normalized * height/2 * HEIGHT_SCALE); } // Detects signal level from buffer data with improved decaying inline function detectSignalLevel() { local magnitude = buff.getMagnitude(0, buff.length); // Use slower decay for a more natural fade out signalLevel = Math.max(magnitude * scaleFactor, signalLevel * SIGNAL_DECAY); signalLevel = Math.min(1.0, signalLevel); return signalLevel; } // Handles silence or very low signal inline function handleSilence(path, w, h) { // Reset smoothed magnitudes during silence smoothedMagnitudes = []; for (i = 0; i < lastPoints.length; i++) lastPoints[i] = h/2; path.lineTo(w, h/2); path.lineTo(w, h/2); path.lineTo(0, h/2); path.closeSubPath(); return path; } // Creates the FFT path with optimized points and improved frequency distribution inline function createFilteredPath() { local bounds = pnl_Fft.getLocalBounds(0); local w = bounds[2]; local h = bounds[3]; local path = Content.createPath(); path.startNewSubPath(0, h/2); local actualBins = Math.min(DISPLAY_BINS, buff.length); detectSignalLevel(); if (lastPoints.length != actualBins) { lastPoints = []; for (i = 0; i < actualBins; i++) lastPoints[i] = h/2; } // Initialize smoothedMagnitudes if needed if (smoothedMagnitudes.length != actualBins) { smoothedMagnitudes = []; for (i = 0; i < actualBins; i++) smoothedMagnitudes[i] = 0.0; } if (signalLevel < SILENCE_THRESHOLD) return handleSilence(path, w, h); // Calculate the proper bin-to-frequency mapping local totalBins = buff.length; local binFrequencies = []; // Pre-calculate bin frequencies for (i = 0; i < actualBins; i++) { local freq = (i / totalBins) * (SAMPLE_RATE / 2); binFrequencies[i] = freq; } // Now draw the path using the frequency mapping for (i = 0; i < actualBins; i++) { local freq = binFrequencies[i]; // Skip if frequency is out of our visible range if (freq < MIN_FREQ || freq > MAX_FREQ) continue; // Logarithmic mapping of frequency to x position local logPos = Math.log(freq / MIN_FREQ) / Math.log(MAX_FREQ / MIN_FREQ); local x = logPos * w; // Get current bin magnitude local magnitude = Math.abs(buff[i]); // Apply envelope smoothing for each bin // Fast attack, slow decay if (magnitude > smoothedMagnitudes[i]) smoothedMagnitudes[i] = smoothedMagnitudes[i] * (1 - AMPLITUDE_ATTACK) + magnitude * AMPLITUDE_ATTACK; else smoothedMagnitudes[i] = smoothedMagnitudes[i] * AMPLITUDE_DECAY; // Convert to dB for better visualization local dbValue = linearToDb(smoothedMagnitudes[i]); // Map dB to display height local y = dbToDisplayHeight(dbValue, h); // Ensure valid range y = Math.max(0, Math.min(h, y)); if (Math.abs(y - lastPoints[i]) >= THRESHOLD) { path.lineTo(x, y); lastPoints[i] = y; } else { path.lineTo(x, lastPoints[i]); } } path.lineTo(w, lastPoints[actualBins-1]); path.lineTo(w, h/2); path.lineTo(0, h/2); path.closeSubPath(); return path; } // Main update function called by the timer inline function updateFFT() { pnl_Fft.data.buffer.copyReadBuffer(buff); pnl_Fft.data.path = createFilteredPath(); pnl_Fft.repaint(); } // Initialize everything initialize(); }
-
H HISEnberg has marked this topic as solved
-
@HISEnberg Have you checked the perf with the new profiler? This would help finding the bottleneck
-
@ustk Precisely what I am using, it's been super helpful and I really appreciate Christoph's work on this! Unfortunatley it's a really strange issue. Generally (90% of cases) the plugin runs completley fine, and there really is nothing super complex about it. However for about 10% of users there is some serious lag and even crashes their DAW and I can't put my finger on whats causing this (I haven't been able to recreate the issue). So I am trying to minimize the imapct of any UI/Script callbacks and paint routines
-
@HISEnberg Maybe you could just try with the stock createPath to see if it's stable and if the issue is about UI drawing / message thread
Is your interface script deferred?
Long ago I had a lot of crashes doing that kind of things. It's now stable and I reckon I added extra security here and there.
Also when working with a buffer, I tend to create a copy to work with instead of the original. Afraid that another thread want access at the same time (I don't always know if the writeLock is set, so...)
-
@ustk Ah great tips thank you!
My OG script is using the createPath method. It performs well (generally better than the HISE stock display with the floatingTile).
The interface script deferred, does that just mean the Synth.deferCallbacks(true), or are you referring to the specific script?
Interesting suggestion about copying the buffers. I would have thought there was more overhead this way but I could see how this is more safe for threading.
-
@HISEnberg said in FFT Analyser Path - Need help drawing the magnitude to height:
does that just mean the Synth.deferCallbacks(true), or are you referring to the specific script?
Interface script should always be deferred. Included scripts are just part of interface so they inherit from it.
Scripts that are dealing with midi/audio shouldn't be deferred