How to draw super smooth Waveform?
-
@oskarsh Posting a snippet might help.
-
There's a dubious function available as LAF callback that lets you customize the audio rendering options:
const var AudioWaveform1 = Content.getComponent("AudioWaveform1"); const var laf = Content.createLocalLookAndFeel(); laf.registerFunction("getThumbnailRenderOptions", function(obj) { Console.print(trace(obj)); obj.manualDownSampleFactor = 4.0; return obj; }); AudioWaveform1.setLocalLookAndFeel(laf);
By changing the
manualDownSampleFactor
property you can get a low-res signal with less edges which you can then make more rounded using the function you mentioned above. -
@Christoph-Hart Can I do this to the oscilloscope per chance? Or get a similar effect
-
@mmprod Yes you will need to use a scriptnode oscilloscope buffer. Instead of downsampling inside the LAF you will need to downsample the audio signal in the scriptnode network. You can then smooth the path the same.
-
@mmprod
https://forum.hise.audio/topic/11419/help-needed-detecting-audio-signal-in-display-buffer-for-oscilloscope-animationIf this is something you've never done before, this post could be a great starting point. It’s not exactly what you’re looking for, but it’s a good foundation for customizing your own oscilloscope using the one in Scriptnode.
-
@mmprod I am still not done this but I am working on an oscilloscope I plan to share soon with interpolation to smooth the waveform:
HiseSnippet 3945.3oc6Zz1Tabb9D3KoVIoMoMMoebKSSFgMHj.AFhKMHCRFl.HUIY6zZ6g43tURq4zcWu6DfZGOS+X+V+KzeJ4mP+MjeA4a8isOO6d6c6JIvDhMYbmnwCV6tOO6979K6plg91znH+Pib46LJfZj68MaOxKt+18sXdF6siQtOxrQjMy00Ox1OftZwJFOXTfUTD0wHWtYeHBVtaeKC9mu6KefkqkmMMaJCiG6yro6yFvhyls4VeErk0sbncXCTftxV6Y66ssuq+Pfjl0rjQfk8IV8nGZgfMiowtVQ8MxcGy0r2X0RGSKs5pqWYk0qTYsJqsZotkVo6wquVkiW4dUVwoxpksWwH26TygE6G1N1JlFYj6VOv2YT699m4INfGyhXG6RwAkMZCmrX559tNHKhyZrcelqSSorJxvH261LSxMqPx8wlGvbXoymIA+P9BjLLTEf4lQm7lUi7JqRdkTHuoPR4THoaIHoOxrscHKHNaEjddOy87hogcs.8jJoHf0Xl+ymXtsO.gWbwAVmPqGBCRwnPkRkVf.+Y96mO+R2gr4qkO4m1jjFs2du82G9ail0H6rW6l6W8OQdvipWuVKx2qc5574NKA72RjGaExr.EPTdvvLJlbpUHIvy8HvonLYShTP0iFus+f.eOXPg4j.LGHjVZoLDqeNhC2ACwnV2tTa.bXZDRU3lNXywE5KQdvPXpPzhJfFFyzHNo7IC+cXQAtViDH0F7r.sXxlog1QfjFoOwNLAlEJMAB6W6vG1YW.kxkVtx8I.k0h5Lzl5PNVPgtTudw8Ic8CIQwVGybYwiFaOZ1Bzts5rWs1v972xSfOyINv84HO2WnbTKHV+vgC.2KOOpaDrb47uDDKRFnXDMtEyqmXOxDQEzOLfWTwnpcL6TZg3vgz44LBeBvkjD2mlvM4Un7N0Nn4QohZwYUzNjBXTHibEaUGJXXDZENRJUPwQfvkDHz74Co837X68d3gU2G1utVtQTNt64whYVtr+JkXEIlOOd9Q1vjd8paYCwNPEPwRb32Yjm0.lsbc.CD.EBu81U2u1Q6TaavUZSRoharNGu1C78i6mgAmFcz2LtoGOPPfuqULy2iTe7seuC6TqUyF6WsydMN7n5U2tSCT.spf5.81w.+62kD3y7hiHw9DV5FBxYZ7YTpGIxZP.5xwc.YQCsbmts9AU+5id3tMZ2AscVcrinWee.r3PKlqJNa2X+FOp0QO3gbuWLQSTwicgrL2eRfZU8IJPERcD13VmQNy5TJHhFPrgECmDy1GznA2yPhbuPfwTD0fOxqbOp14Qsp1o1NJayHJjI9L99fSIzSQVwCCA4mCIfZchJy1tSqFeUsidxd6zYWz0d4DyjIoAP4aehGXQdAXurhQ1CyjrSEuF0q2tVmi3lZ7fCI38XTAB1Rf1oK3ywsqnbN3BzUBoHP.GtSim.tG+Y91s5XVrmw7b7OiDgNI3lY4EyVDrXs3dWJ6lPdhFlc1sUs161X+c39.BWfN8CoQ8gz9pxTvHmaEtM20lzzJtuJABEEgSojHPDC.mrfV3xPqytLHQBfuNd1gJFXJaAWHwo.XWd5ygXdHvEXHObeBi76U7Gfw28tySxKBnlgYwfgQ8KLMR.n1WlmGaoaHTr019C8h46Lm+qhxzpIxTRclK3zRpOzylKg3QjjlTBZi4AgL.8QBHIKmjIQDGbgDMWaPwkRpiyRBPKJRjH4JDPA33GWezlJZ3.A4pOscFeHWfeDu.lawC.NuXWWe+vBYjBYIxxfB4Eje+ljKEfTRQmbvOrtjB.WbWxKlm7GfCm74eNQNdLdRYOlbevOHic2MSP5o7M442eBn374cuq9BuL+jeSpmdJ64fH.27kDHKPU.XHEr98RgkaZ.VA6CpTLHudN.oUv3pbkH6sEAzSU7hjLoJcgpRAdoUzqzhfrHo7zsJT2MgUuTB974mhsPYtFOgvtbkqfZQipW.xtDLzU+W7YStCoPYfpimGTjJ5zx3Jwh.AIhYlpXdLMo3uW34LgLRxyIJVUDkJ2ES+.Qh..Hs7GFiJzrExKKrEKYhCTBLEjJ8B8xB5TrKziWUW2BoobwxWktG+1rJdlP2InRQAPcId9Pr8ddVtKPrN0m4PFBU9gEOgkT4.AKwv7BgB++vnQmwb3AagLDQXkrOAGWHQHf.zmx50OVAhc4SnBx.lCViTBjfSeB0iIe5y5Fmj0JfmSPyRUotDg8I4OvsdWbwIX0rPyBGR0wbE28UYsj7MEscACDIsljZIaRIUtsePZMmNVwVPlQmDeZBKlCUZMvPy8iZQsbRhPqTeqhDQFOHo+iM0ipqfyBSI0sx9nZ9ktWSIbg94svTqtTkesbsGxKjzY5kAKOerLo8omRcgiUgpQyflvRsr75QK.s4pTI+SkZhwq7lmhXf04EJUr7BIiXdEzA6Np0cu.oLXMkRCyqvA6fo9wNNzJAHU7yEK5z7SK8bcq.nOqv3Com0d3w7D6.avMjWTh9cDiuiNmHIhTa3xhnsYhfoGlEoKraU.mkTkWv9y8AyBLNUpmojJC2pQv5WExMCKIei4d53W37EHilWymQUrpkhQS9lRcSZalJhkdeulEwSdhWZ4NZR7KD2KSALEV7GtdPJbtP8fRjsRXjtDDFy3WsS.fAfHUmp20vqLR6lWTnVdA6AV1r3Q7x9WUlKFEjYaz75RB4w2Pzyxl5c2bGjLVTkL3EGlEdGyTJZdqfrGNjqp5Fz2pfVyhKHIt40v2Iw.ufVlhEHOEr5zosEDJ7ERxXAv72lqir8r49B814d4E6jLQrGMlHsy3jMPgBS9eAsU5UQNfB3kSKzWZiDWry5THHgLbRZJwJ6JRSZ8KiT2KStwMkhivarNDy03drk8IpUGo0Wn.rMI075AdDIsZwmrwwu.uOOXmkfgrC+KxcMqdpzxovZlx5L6t2k7YPZD7VRKMggt1cIU+b9kSNLLD53imvQbKWXIIEKUpLuJqGE3foMgyPToEwheeXo6XZcegz.rrOri0DyGoDpMFTjq.iQNQg2vE3esvJ3UHl1efu2g9wzF.OBr3sgpPIiuT2tScMr80PeWWXGm1xhy5RPrfG+Rh.+GKWPRHAzH2szuKdyK9t3Uep.aQ2zJ.56g2aWi.p2E8.BFIsfCe6Q6sCTaFdA9IykcUWvb6POkYSEWm+sM2gFcRrefQtYRusYibuSLe0OPdY+VdTWCliQt7lR8lw4puHyH0AbGhrIFskv0HcluIbKCkC68iSdrCwgU20GZOwqWGFHWvy7WXpN0Edv+q3sdUG7e+OtUh.Qv84M+J5ni8sBcLNtm7IhdeyJKudk6s1FqrdICVLcfbg2yrxpKuw5UVekxJyur.iMprwZ2a4MV0Hlddr5qMw0Dy77blnyzbv4w6a.ui4JEKgW97bIWIdC6XHfj3xFgk4WLKe888OCPClZC9vcYhQkW9d7waOLJ1evCCsBfnNQZXtCsq0P23pAPkgg3inAqhdp7Ee.dCkvV0BaFjG7bsM3eVe8R2aiR2qLGpN9854ROv2gpsy3yOkbe4Hovm6QQzGSwL3JzR5oAKVGB61Ndj6X6TyZRkv3yy8+yNkkkyWyyQ4rWK+KMLl7gp9PSfnwR20e2L70ESV.bf0drJ7NH7hfrkpu93qsGS6pRhejYSVrc+oSiyLEZDbyeSPiIOA4GXJdqnLB7Vl0+52Pu23rJm+6JN+2yrs3ZSSdow2OabY8mZL81UUEMOgFWc.lcSUm1Ljp+Hy.iCEBM9b4l8JyWk+d+NpSU2+olYbA4GGqTwsuzCZ5LjAAZMg3Qh22S95Xvb4lAy1HFWBGiDQapmCev+E9jrXYbbtjEKKWT0JyTFHNI1+WKMwNWioApP6gCiR2XwCLm6eXlkTN.qnHD5XnI.eAQuFPPN5Bjic8sOge2vSjIO4wyd.BQA6Dd85jv+cdMYzbinIxJY3PZ7Y9gmvsVS9N3CJ731IJPNUU7shvb2rDSSASAZD9f20DJ.rXIAXM8cGEz22iYipPARRhW5TJ3fcsh5.cmgA3ZOLBpwwogWa.X9O6CH.StCg7OfnUzf3Hr5aLPIVkD35fOSZpKjF0lYBgxXwO3CM.v8UlrVH7.xb.upjYLArv0EhDAAHNAenIIgYkgFE8IJTjBLSmJT.35PE+bSekeCMiSGVPA2ihnE0fYZzwXayqhRPknK8bY0ki4SN9LFX9JG54hCrF3u33Pc3HiamQSKr4iXpL0irDU8UfnmxQ.Qc.y6wnGG7UzVy3.qySGCETYzNlFfd333xvXgrpgTXCNTFoviqqct7.03nQHoy006EIehQIdfE6ER3W0M35Tf9TJf3WatKT1jMnzbHGXEwe.MQDTSHBpV9wYRcp0b.dski6lNwwmJKO3IPQy7eWJb99WyGye94GRA+vIqSPOK++9p9yKK3J+yKSTGeGnd6n.+HneEkctMc.qCz1Sj1rigwxSECsY2gFOzSeqESIgZqUVY1uAEE0AQgJbaImTY+93sfF7z1LXr14c.S1zE4Y+yu8Kq4g+9kZSAN2Q7S4iWelZIVCg534c5Td5Sur5znULnCsurput0Uyx7U7ir6cdS8ir6VWsJttbx6sf1Xzpk81RZrMCyKTy6TpKlFAoweoYR2mxY0cCOv2yWVbPlRuEEhdzqGUyVZpLT03Xn8UUa3VTWpUjhq4uaKwqdd.Oq40RV7Zpt9eiofb4Wz2OnB6uQMytzVQm8J0J5qc5852V5MrGJDx7sGM8mh+hrWlvU2je7022v0R7qj1TUG5v722GKEiKV9XwLDbJB1MIUuXhYdiWLAlUb73WbsDTsf8IX0rJGERlhTyNpv2x2OFu6a0eq7hmCmeAav1qGBFqUlpMWcnWLQ+BiL29Kd1ih.Hd1.1Iz+5yRpN8YbAkXWe1ZKW5dqs9QGY0e.CKT9EGczfgQL6EiwRbXnkUOX5Ei.6jEEq.UrsH9D4gKdVeZHcwyv+4Eu3Yg.rEGDrhw.TRKzxCrPihbdeVICWfk4bgbMWtLvQt9alpHt0OUEwOVUQr7OUEwOUEwa8UQbCmf6mmdW5fobVyxeBeLA6H7B5Vdla5tkmZqupShuiTH8uvezJ04212JbpK8ceYcfImJNYW1t1aD7uB17a+x2xRb7F+YodqLYwJ+Txhe3IK9w8EOm8+qewyajDA2Dmw.K6P+irEuMFZm9y3y.7sGuugaad.NlT13TwkfmoKG.Q4Nx1Veql.wkutHtx0EwJWWDW85h3ZWWDu20Ew0e0HhIgpNL1efv0zv3fl0DOgStzdMyMqw+CFi.0X
This was vey much inspired by @Mighty23 's approach but with some distinct differences. To be honest I was struggling with it and used chatGPT to figure out the anti-aliasing filter and linear interpolation functions, i.e. it isn't completed yet. Use a noise signal and compare the green with the red output to see the differences.
Perhaps you could employ @oskarsh suggestion with downsampling the signal for further smoothing. Also if anyone can figure out how to compress the signal on the vertical axis based on the amplitude would be a true hero.
Content.makeFrontInterface(400, 400); /* ====================================================================== ==================== OSCILLOSCOPE DISPLAY BUFFER ======================== ========================================================================*/ // Variables const var pnl_Osc1 = Content.getComponent("pnl_Osc1"); //const var Fx1 = Synth.getEffect("Fx1"); const var Fx = Synth.getEffect("Fx"); // Your ScriptFx with an fft analyzer to reference. // Buffer Properties const var BUFFER = Synth.getDisplayBufferSource("Fx"); const var BUF_OSC1 = BUFFER.getDisplayBuffer(0); const var BUF_LENGTH = 1024; // Reduced buffer length for stability const var BUF_PROPERTIES = { "BufferLength": BUF_LENGTH, "NumChannels": 1 }; BUF_OSC1.setRingBufferProperties(BUF_PROPERTIES); BUF_OSC1.setActive(true); // Activate the buffer const var TEMP_BUFFER = Buffer.create(BUF_LENGTH); // Temporary buffer for processing reg BUF_SIGNAL = false; // Initialize as false var scalingFactor = 1.0; // Dynamic scaling factor const var SCALE_DECAY = 0.98; // Smoothing factor for dynamic scaling // Interpolation Factor const var INTERPOLATION_FACTOR = 50; // Number of points to interpolate between samples // Visual Properties const var MAX_GHOSTS = 5; // Number of ghost trails const var COLOUR_BG = Colours.black; const var COLOUR_RAW = Colours.red; // Raw waveform color const var COLOUR_SMOOTH = Colours.green; // Smoothed waveform color const var COLOUR_SATURATED = Colours.yellow; // Color for saturated peaks const var STROKE_WIDTH1 = 2.0; // Smoothed waveform thickness const var STROKE_WIDTH2 = 1.0; // Ghost trail thickness const var OFFSET_SCALE = 10.0; // Vertical offset scale for ghost trails const var SMOOTH_WINDOW_SIZE = 15; // Smoothing window size for anti-aliasing const var SATURATION_THRESHOLD = 0.8; // Threshold for saturation // Create Paths const var oscPath = Content.createPath(); const var rawPath = Content.createPath(); // Path for raw waveform const var ghostPaths = []; for (i = 0; i < MAX_GHOSTS; i++) { ghostPaths.push(Content.createPath()); } var frameCount = 0; // Anti-Aliasing Filter Function var smoothed = []; inline function smoothBuffer(buffer, windowSize) { for (i = 0; i < buffer.length; i++) { local sum = 0; local count = 0; for (j = -Math.floor(windowSize / 2); j <= Math.floor(windowSize / 2); j++) { if ((i + j) >= 0 && (i + j) < buffer.length) { sum += buffer[i + j]; count++; } } smoothed[i] = sum / count; } return smoothed; } // Linear Interpolation Function inline function interpolateSamples(buffer, factor) { local interpolated = []; for (i = 0; i < buffer.length - 1; i++) { interpolated.push(buffer[i]); for (j = 1; j < factor; j++) { local t = j / factor; interpolated.push(buffer[i] * (1 - t) + buffer[i + 1] * t); // Linear interpolation } } interpolated.push(buffer[buffer.length - 1]); return interpolated; } // ---------- Paint Routine ---------- pnl_Osc1.setPaintRoutine(function(g) { g.fillAll(COLOUR_BG); if (!BUF_SIGNAL) { return; // If no signal, avoid unnecessary drawing } var width = this.getWidth(); var height = this.getHeight(); var midY = height / 2; // Shift ghost paths for (i = MAX_GHOSTS - 1; i > 0; i--) { ghostPaths[i] = ghostPaths[i - 1]; } oscPath.clear(); rawPath.clear(); // Copy buffer data and smooth it BUF_OSC1.copyReadBuffer(TEMP_BUFFER); var smoothedBuffer = smoothBuffer(TEMP_BUFFER, SMOOTH_WINDOW_SIZE); var interpolatedBuffer = interpolateSamples(smoothedBuffer, INTERPOLATION_FACTOR); // Calculate dynamic scaling factor var peakLevel = TEMP_BUFFER.getPeakRange(0, BUF_LENGTH)[1]; scalingFactor = Math.max(0.1, Math.min(scalingFactor * SCALE_DECAY, 1 / peakLevel)); // Draw the raw waveform var sample = TEMP_BUFFER[0]; rawPath.startNewSubPath(0, midY - sample * midY * scalingFactor); for (i = 1; i < BUF_LENGTH; i++) { var x = (i / BUF_LENGTH) * width; sample = TEMP_BUFFER[i]; var y = midY - sample * midY * scalingFactor; rawPath.lineTo(x, y); } // Draw the interpolated waveform sample = interpolatedBuffer[0]; oscPath.startNewSubPath(0, midY - sample * midY * scalingFactor); for (i = 1; i < interpolatedBuffer.length; i++) { var x = (i / interpolatedBuffer.length) * width; sample = interpolatedBuffer[i]; var y = midY - sample * midY * scalingFactor; oscPath.lineTo(x, y); } ghostPaths[0] = oscPath; // Draw ghost trails with vertical offset for (i = MAX_GHOSTS - 1; i >= 0; i--) { var opacity = 0.5 * (1 - i / MAX_GHOSTS); var verticalOffset = OFFSET_SCALE * (i - MAX_GHOSTS / 2); g.setColour(Colours.withAlpha(COLOUR_SMOOTH, opacity)); g.drawPath(ghostPaths[i], [0, verticalOffset, width, height], {"Thickness": STROKE_WIDTH2}); } // Draw the raw waveform g.setColour(COLOUR_RAW); g.drawPath(rawPath, [0, 0, width, height], {"Thickness": 0.5}); // Draw the smoothed interpolated waveform g.setColour(COLOUR_SMOOTH); g.drawPath(oscPath, [0, 0, width, height], {"Thickness": STROKE_WIDTH1}); }); // ---------- Timer Callback ---------- const var oscTimer = Engine.createTimerObject(); oscTimer.setTimerCallback(function() { if (frameCount++ % 1 === 0) { BUF_SIGNAL = Fx.getCurrentLevel(true) > 0.001; // Update if signal active pnl_Osc1.repaint(); } }); // Start the timer oscTimer.startTimer(30);
-
@HISEnberg @oskarsh @Mighty23 thanks guys- I’ll look into these and get back to you
-
Ok - I put a "sample and hold" node before the oscilloscope (is that what you meant by downsampling? @oskarsh ) and drew the oscilloscope path like @Mighty23 and @HISEnberg recommended. I got some decent results but unfortunately now HISE crashes after 5-10 seconds of opening! My hunch is that the timer interval is too fast or that the buffer size is too high. When I delete the code and the panel that is being painted, HISE doesn't crash.
//code thanks to mighty23 const var dp_analyser = Synth.getDisplayBufferSource("Script FX2"); const var rb_analyser = dp_analyser.getDisplayBuffer(0); const var BUFFER_LENGTH = 512; const var properties = { "BufferLength": BUFFER_LENGTH, "NumChannels": 1 }; rb_analyser.setRingBufferProperties(properties); const var P1 = Content.getComponent("P1"); const var NUM_WAVEFORMS = 6; const var waveformBuffers = []; var time = 0; for (var i = 0; i < NUM_WAVEFORMS; i++) { waveformBuffers[i] = rb_analyser.createPath(P1.getLocalBounds(0), [0, BUFFER_LENGTH, -8.0, 8.0], 0.0); } var bufferIndex = 0; var offset = 0; P1.setTimerCallback(function() { var newPath = rb_analyser.createPath( this.getLocalBounds(0), [offset, BUFFER_LENGTH + offset, -12.0, 12.0], time * (bufferIndex + 1) * 0.2 ); waveformBuffers[bufferIndex] = newPath; bufferIndex = (bufferIndex + 1) % NUM_WAVEFORMS; offset = (offset + 256) % BUFFER_LENGTH; time += 0.1; this.repaint(); }); P1.setPaintRoutine(function(g) { var a = this.getLocalBounds(0); g.setColour(0xFFB8C043); g.drawPath(waveformBuffers[i], a, 1); }); P1.startTimer(1);
Any ideas?
Otherwise I may stick with a non-smoothed oscilloscope for this project - this seems like a very finicky process.
-
@mmprod i was getting instant crashes too, I suspect the buffer doesn't clear so it just fills up and crashes. Did you try my version? I basically bypass the buffer altogether and just refer to the fft and draw it to the panel.
-
@mmprod yes the sample and hold node is correct. Go a little easy on the timer. 10 or 20 should be enough.
The problem with the oscilloscope is that you draw many points that are not necessary basically overlapping points. To get better performance you need to simplify the path and drop points that are the same. There are many discussions on the juce forum about this
-
@mmprod said in How to draw super smooth Waveform?:
Any ideas?
I introduced a simplifyBuffer function that removes redundant points from the buffer based on a
SIMPLIFICATION_TOLERANCE
value.const var dp_analyser = Synth.getDisplayBufferSource("HardcodedMasterFX1"); // or what you neeed const var rb_analyser = dp_analyser.getDisplayBuffer(0); const var BUFFER_LENGTH = 512; const var properties = { "BufferLength": BUFFER_LENGTH, "NumChannels": 1 }; rb_analyser.setRingBufferProperties(properties); const var P1 = Content.getComponent("P1"); const var NUM_WAVEFORMS = 6; const var waveformBuffers = []; const var SAMPLE_REDUCTION = 4; // Only process every 4th sample var time = 0; for (var i = 0; i < NUM_WAVEFORMS; i++) { waveformBuffers[i] = rb_analyser.createPath( P1.getLocalBounds(0), [0, Math.floor(BUFFER_LENGTH/SAMPLE_REDUCTION), -8.0, 8.0], 0.0 ); } var bufferIndex = 0; var offset = 0; P1.setTimerCallback(function() { var newPath = rb_analyser.createPath( this.getLocalBounds(0), [Math.floor(offset/SAMPLE_REDUCTION), Math.floor((BUFFER_LENGTH + offset)/SAMPLE_REDUCTION), -12.0, 12.0], time * (bufferIndex + 1) * 0.2 ); waveformBuffers[bufferIndex] = newPath; bufferIndex = (bufferIndex + 1) % NUM_WAVEFORMS; offset = (offset + 256) % BUFFER_LENGTH; time += 0.1; this.repaint(); }); P1.setPaintRoutine(function(g) { var a = this.getLocalBounds(0); g.setColour(0xFFB8C043); for (var i = 0; i < waveformBuffers.length; i++) { g.drawPath(waveformBuffers[i], a, 1); } }); P1.startTimer(20);
Fix this value according your "downsampling":
const var SAMPLE_REDUCTION = 4; // !!!!!!!! Only process every 4th sample
We need to optimize the scope design; this version could be a starting point. You can adjust the
SIMPLIFICATION_TOLERANCE
value to control how much the buffer data is simplified, "skipping" redundant points.That said, I realize this discussion might be going off-topic and potentially not fair to other users. Let me know if it would be better to start a dedicated thread for this topic.
-
Thanks guys for the help- the closest I got was simply using the original code with the higher timer value like how @oskarsh suggested. But of course the animation is more choppy.
@HISEnberg and @Mighty23 I tried both your methods- these work well but I can’t wrap my head around how to ‘restyle’ it to a ‘smooth’ waveform. I’m also getting the occasional HISE crash which may be a problem for future plug-in users.
Im going to use the vanilla oscilloscope for my project. Once again, thanks for your generous help!