Drawing Level Meters - Visualizations with JUCE

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
hello welcome to an edition of visualizations with juice today we're gonna build this it's a level meter a staple of modern plug-in ui design the cornerstone of all things flashy in your vsd the market today is over saturated with tons of plugins with flashy and sometimes pointless level meters but we're going to learn to build one today nonetheless the main point of the level meter is to give the user some sort of visual feedback as to what's happening within the plugin you can use what you learned today in a variety of use cases instead of just putting it for the input and output levels you can use it to measure detection in certain effects and frequency bands you can use it to see what the side chain levels are when compared to the main bus this is not however a video on how to build a loudness meter now that's far more complicated to measure loudness and they're usually present in fully featured mastering plugins where you actually need them no sir today we are going to talk about simple rms level meters we are going to build three meters today i'm not going to show you how to do the final one the circular one but if you're interested you can check out the source code of this plugin on github in the description below the horizontal meter is the simplest one and it's really easy to do the other two are not so hard either they're sort of linear gradient sort of meters the difference between them is that one is continuous and the other is discrete this one might look discreet as well but it's a cheeky visual trick to actually paint an image of a grill on top of whatever is being rendered of these are dynamically resizable as well except for this one here since we're actually drawing an image and we can't actually resize a raster image without it losing resolution which is one of the disadvantages of this method so what are we learning today we'll be learning about components and its responsibilities who is responsible for generating the level value and who is responsible for actually rendering the graphics we'll talk about rms level what it is and how to calculate it and finally we'll talk about the graphics class in juice and how to render these animations efficiently before we start off if you want to see more content on audio programming or generally about digital audio do consider subscribing it'll help the channel grow before we start writing any chord let's just think about the problem holistically what are the components in our code what's their responsibilities and how they interconnected with each other so we've got the plugin processor and the plugin editor the processor is responsible for processing the audio this runs on the audio thread and is time critical it has a process method which is called back by the underlying device driver whenever the audio buffer is full and it is the responsibility of the plug-in processor to process this buffer within a certain amount of time there is only ever one instance of the plugin processor class and it pretty much outlives all other objects the plugin editor is responsible for rendering graphics onto the screen this runs on the message thread and it's more relaxed here there are no strict time constraints the plugin editor can be destroyed and recreated anytime and more than one instance can exist but generally you have one instance of the plugin editor per running application now the plug-in processor is only concerned with itself it is oblivious to whatever happens around it the plug-in editor on the other hand is dependent on the plug-in processor it has a it has an instance of the plugin processor and the editor can call and access methods within the processor but not the other way around right so if you want to render a graphical meter within the audio application then you need to do it within the plug-in editor but to keep responsibilities small and compact we can pull out the graphical mirrors into a new class let's call it level meter the plug-in editor is composed of the level meter great now the level meters need a level to work with where is this level value going to come from it has to come from wherever the audio is available so it has to come from the plugin processor the plugin processor essentially has all it needs to calculate the audio level values and this is a simple calculation called the rms value or the root mean square value we'll talk about rms in a little while but the plugin processor calculates updates and stores this value on a regular basis the plugin editor will routinely fetch this value from the processor at a visual rate say around 24 times a second which is just like 24 fps and feeds that value into the level meter class the editor essentially acts like a controller or an orchestrator in this case it gets the value from the source and passes it off to the destination and that's the flow we are going to implement quite simple right all right let's get into the audio level calculations done first so the plug-in processor goes through audio samples at the sample rate so for example if the sample rate is 48 kilohertz then you'd have 48 000 audio samples per second that's an overload of information we we don't need anything even close to that level of granularity when we want to visually represent levels so what we can do instead is get an average of values over a certain duration and then that will be our audio level for this we can use rms calculations what's rms it's root mean square the definition is in the abbreviation it's the root of the mean or the average of the squares of the values all right but all we really needed was the average you can get that by adding a few values and dividing it by the number of values added why are we doing this dance of root mean square to get the average the main reason is that audio sample values can be negative or positive so consider a simple sine wave which swings from negative one to positive one conventionally calculated what's the average well it's zero because the positive and negative values cancel each other out so it's kind of pointless to calculate the average then if it's zero for these kinds of polar signals what if we calculate the rms value instead we get something like 0.707 for a sine wave of one period why is that that's because squaring the value will actually get rid of its sign it'll convert the value into a positive number and since we have squared the values we take a root of the average to balance the equation the resultant value seems like a fairly accurate representation of the work done by the sine wave over time and work over time in physics is the unit of power and it's precisely what the rms calculation is meant to represent the electrical power in signals of course electrical power in audio signals is not equal to loudness there's a strong correlation though when the power is high loudness is generally high as well but it's not a perfect measure of loudness nor is this video meant to be anything about loudness measurements but rms value is close enough that it's widely adopted and used in most audio plugins and applications to represent audio levels rms calculations are quite inexpensive as well when you see level meters in dw softwares like this they're essentially representing the rms level of the underlying signal there's only one variability when calculating the rms value and that is the number of samples to take an average off you can either think about it as the number of samples for for averaging or as a time period or a window of time for averaging the signal within that period so for example if your sampling rate is 48 kilohertz and you calculate the rms value of the signal for every 480 samples you get 100 rms values of the audio signal in one second and each window of measurement is 10 milliseconds so if you reduce the window of time to let's say one millisecond you get more number of points and hence more granular measurements for rms over time the lower the time window the more responsive the level values are but they can also get jittery and inaccurate during transients and bursts of volume when the time period is too small and on the other end if you increase the time windows for calculation we get a more sluggish and lazy response to the audio signal but you end up getting a more smoother range of values ideally you want to select a good sweet spot where the visuals are smooth yet accurate instead of giving the control for the user to choose the time window so what do we choose how many samples do we select for calculating the rms in this video we are going to keep things as simple as possible beggars can't be choosers so we are going to calculate the rms for however many samples that the audio callback provides for us this will keep things simple yet interesting if you want to choose a different number of samples then you'll need to maintain a data structure something like a lock free queue where you can dump values in and read it at a later time when it's filled up you can refer to the gita project for this example plugin to see how i've done that most of the time i found that relying on the number of samples that are provided by the audio callback is good enough you get a decent resolution and smoothness and we can apply a bit of smoothing later as well let's start off and create a new juice project i'm going to choose a plug-in project just because it's easier to test i'm going to call this level meter i'm going to leave the defaults as is let's create a new project save it where it suits you we're going to change a couple of options in the settings pane i'm going to give the company name as tilt audio i'm going to enable this option here which is going to add an extra line into the juice header file using namespace juice this will just avoid me having to type the juice scope resolution operator before using any of the juice classes right let's kick off visual studio before anything else i'll quickly set up the project for local debugging i'm going to select the level meter vsd3 project i'm going to right click and go to properties and over here for debugging configuration and for the x64 platform under debugging i'm going to change the command attribute from target path to the path of reaper's executable you can put the path to the exe file of whatever diw software is installed on your computer i like repo since it's simple and it's really quick to start up so i use it click ok i'll right click on level level meter vst 3 again and set it as my startup project that being done you can actually see it in bold now okay i'm going to start it off the initial build will take a minute or so but subsequent bills after that should be fairly fast so it should open up or an instance of reaper for me automatically after the build succeeds now i'd like to just create a new project just to test this metering plugin that we're building over the entire life cycle of its development i'll just add a reference track just for the crack i'm going to add a new vst plug-in to this track let's find a level meter there it is just make sure that the path to wherever you have saved this project is present in the vst search path of your daw software rescan it if you have to now there we go we haven't actually done anything yet all we have done so far is just set up everything we need for quickly testing and validating our changes let's get coding shall we or head down into the source on pluginprocessor.hedge i'm going to create a couple of member variables here to hold the state of the rms values a floating point value for each channel essentially so rms level left and right and i'm going to head into this source file i'm going to go down to where the process block method is i'm going to remove quite a lot of the code here it's not really needed for us a nifty little method to actually calculate the rms here in the audio buffer class it's called get rms value and accepts a channel so you pass in the channel that you want to calculate the rms value off the starting offset which is going to be zero on the number of samples you want to calculate and that's in the buffer dot get numb samples that's it you get the rms level of each channel so i'm going to duplicate this for the right channel as well and change the channel to one get rms level method is it's actually quite simple right so if i go into the method you can see that there's a sum and you're accumulating the sum with the square of the sample so sample time sample and then after that in the end you're taking a square root of the sum over the number of samples which is just the average so you're taking the square root of the mean and that's all it is if you want to write your own function go right ahead you don't really have to use the get rms level method here but it's quite handy it's quite small and concise and that's really all you need to do to get the the rms value out and as i said earlier the rms value is going to be calculated over the number of samples that the audio callback provides for us so that could change over time but generally it's around 200 to 500 samples so i usually get 440 samples or so we just need to do one last thing here the range of values that you get from the rms level calculations range from 0 to 1 which is the usual floating point gain values that you get for audio signals but this is not a great metric for representing levels mainly because it's linear so you need a logarithmic q to it so you need these values in decibels so that the representation is more fair and accurate again there is a handy convenience method for this it's in the decibels class in juice and it's a static method called gain to decibels so it'll essentially convert the gain values that you do that you get from the rms level calculations into decibel values right now let's think about writing the actual level meter class i'm going to create a new file and a new class for this so we're going to go into the producer project create a new group called component let's create a new header file i'm going to call it horizontal meter for the first one now save the producer project and it should reload all the files there firstly i'm going to include the juice header i'm going to put this class in the gui namespace like people call it gui that's ridiculous gui i'm gonna create a new class horizontal meter and it's gonna inherit from the component class the juice component class could be thought of as the parent class of most graphic components components that need to be rendered out to the screen so one of the member functions that i should override is the paint method this is where we're actually going to draw the meter out onto the screen since this meter has to react correspondingly to a value of some sort i'm going to create a member variable called level i'll set its initial values at negative 60. we want the graphics to show decibel value from negative 60 to positive six decibels full scale that's the idea anyway you can change this around to suit your application better i'd say i'm gonna go from negative 60 to six decibels of this and i'm going to expose a setup function where you can set the value of the level in the paint method is where all the magic happens firstly we'll get the bounds of this component now we haven't actually set the bounds anywhere we'll assume that the bounds of it will be set elsewhere and it'll be the exact size of the level mirror firstly we want to draw the background track of the mirror this is going to be a darker shade of white so you can say colors are white with a reduced brightness of let's say 0.4 i'm going to fill the entire bounds of this with this color i'm going to fill it as a rounded rectangle and now to actually draw the rectangle that actually moves that's going to be our level meter essentially so i'm going to set this color to bright white so we know that the level values will be anywhere between negative 60 to 6 decibels so we want to translate this level value to be the width of the new rectangle that we're going to draw if the value is negative 60 the width of the new rectangle that we're gonna draw it should be zero pixels and if it's positive 6 the width of this new rectangle that we're going to draw should be the entire width of the component so i'm going to use the jmap method to actually translate from one range to another so jmac accepts the actual value that we want to translate level the source the source goes from negative 60 to 6 and the destination range that we want to convert it to is from 0 to the width of the component since this is a templated function make sure that all of the arguments have the same types all of them have to be floating point numbers we'll cast it wherever necessary now all we need to do is fill up this rectangle with its width determined by the value of scaled x so it's a very simple thing to do right so we take the entire bounce and we remove a part of it starting from the left so when you call bounce or remove from left what you end up getting is another rectangle with its width as scaled x we're going to set the same corner size as before as 5 pixels now we'll head into the plugin editor where we actually have to make use of the horizontal meter that we just created so i'm going to include the header as component slash horizontal header.hedge we will create a couple of instances of this horizontal meter class call it horizontal meter l and r for left and right the plugin editor is essentially the parent and these two these two meters that we have created are going to be child components within it let's go into the source file to display these meters to display any component in juice you have to do two things one you have to add and make visible this child component it's going to attach this child component to a parent component and then it will set its visibility to true the second thing you need to do is to set its bounds so we'll go into the resize method we'll actually set the bounds of these two meter components the setbounds method accepts the arguments to a rectangle so the first two will be the x and y coordinates of the top left point of the rectangle um the third argument will be the width and then the height so i want to say 100 pixels left and 100 pixels down and then let's say the meter is uh 200 pixels in width and about 15 pixels in height the second one is gonna have pretty much the same dimensions except move the meter down about 20 pixels from the from the top one now in the paint method we don't really need anything here we can fill it with an opaque background color let's use dark gray here the final thing to do is to actually refresh the meter several times a second so for any animations to happen we need to repaint these meters over and over again for that we'll make use of the timer class we'll inherit from juice timer class and once we do that we need to implement the pure workshop function called timer callback this timer callback method will be called at the desired rate so what do we need to do in this method right so we need to repaint the meters of course so we'll just say horizontal mirror left dot repaint and the right one as well now just repainting it isn't enough we need to set the value right before we paint it so we need to get a hold of the rms values that we just created and instantiated within the plugin processor class let's actually go back to the plugin processor and implement a getter method of some sort to get the rms values i'm going to call this get a get rms value and it's going to accept and it's going to accept a channel count so it's essentially going to give me the rms value of the channel that i request in the implementation of this i'm going to assert that the channel is either 0 or 1 because we accept only stereo if the channel count is 0 we'll return the rms level left if the channel count is one then we return the the right value otherwise we'll just return zero let's go back to the timer class and now we'll make use of the audio processor we'll fetch the rms value from the getter that we just created so audio processor to get rms value of zero for the left channel and of one for the right channel now that we have filled up the timer callback method we need to actually start the timer so that the timer callback method will be called back at a certain interval we need to start the timer within the constructor we can either use start timer or start timer head z um start timer accepts time in milliseconds and hertz accepts the inverse of that it'll be one over the time that's slightly more convenient because if i want to say render the animation at 24 frames per second i can just use start timer head c of 24. instead if i were to rely on the other method i'd have to write start timer of 1000 milliseconds divided by 24. alright i think we have most things here let's give it a shot [Music] beautiful as easy as that before we move on to the other meters i'd like to just point out that it's a it's a bit jittery at the moment take a look it's not very smooth the movement is sort of erratic and there's a lot of urgency in it so we want to just calm the meters down a little so we want to introduce some sort of smoothening such that it's not as erratic or jittery as it is currently there are plenty of different ways of smoothing this out as i mentioned if you just increase the window size you would get a smooth value but we can implement something better for the current window size so here's the strategy for smoothing if you've got a value and the next value is larger than it then you essentially don't do any smoothing that's because if you try to smooth values from one state to another when the value is actually going up you get a very lazy and sluggish response for transients so let's say there's a thwack of a snare drum or a kick drum you want that responsiveness you want the visual meters to actually give you the response immediately when the value is going up we don't do any smoothing but when the value is going down that's when we actually apply smoothing if we do that we get a responsive meter during transients and when the rms value is subsiding the level visualizations decay over time so let's implement this in code now we can make use of a class called linear smooth value this class is like a swiss army knife this can be used in several different situations for all sorts of reasons as the name suggests it linearly smooths values if you haven't used this class before we'll explore it here all we did here is change the rms levels left and right to linear smooth values instead of just plain old floating point values once we do that we have to call the reset method on it whenever the sample rate changes so the best place to call this is within the prepared play method the prepare to play method is where all the initialization code happens so let's go ahead and reset the values here it accepts the sample rate and an amount in seconds for the dk time it'll essentially mean if you set one value and if you say go to another value it will go to that value in the amount of seconds that you determine here now we'll set the initial values for both of these levels we can make use of the set current and target value method to set the value as negative 100. down below in the process block let's focus on just one of the values the left value let's pull out the decibel rms value first then we'll actually do the comparison like i said before if the new value that you're getting is actually less than the current rms value of the left channel then set the target value as the new value when you're setting the target value you're actually saying do the smoothing essentially your current value is some value then you're providing a certain target value over time within the dk duration the current value will tend towards the target value if the value is greater than the current rms level then we set current and target value this means it will just directly jump there won't be any smoothing that happens here now these values don't smoothen on their own you have to explicitly call a method to progress time and to move the values usually if you're doing it sample by sample you call the get next value method on it since we're accessing by block and not by samples we're going to call the skip method and pass the arguments as the number of samples this means that every time the value is smoothed over the number of samples that you provide now down below in the getter method we were previously returning the floating point value to get the floating point value out of the linear smooth value instance we just call the getcurrentvalue method and that's pretty much it we should have achieved some sort of smoothing now let's find out i'll build the application again and we'll see what the smoothing looks like [Music] as you can see when the transients hit the level meters jump forward but whenever there is silence or when the sound settles down the values are smoother in this way we get an advantage of a smoother range of values while retaining the responsiveness and chirpiness of the of the meters all right so on to the second meter then so this one is a vertical meter it's sort of got a linear gradient going on here so at the bottom there's green and at the top there's red and the middle is yellow this is the strategy behind drawing this particular meter is we're gonna we're gonna draw something continuous that goes from green to red and on top of that we'll paint a grill of some sort just an image of a grill and then we'll overlay that on top of the meter that we draw that'll give us the perception that it has holes in it and it's sort of illuminating through the holes so we'll start off by creating a new file as usual we'll call it vertical gradient meter and we'll save it we'll set up this file as we did previously we'll include the juice header put it inside the gui namespace and create a new class here called vertical gradient meter it will extend the component class from juice over here we'll override the paint method i want to try and adopt a different strategy of getting the values in this class previously we had a we had a private member called level we exposed the setter method to set the level value this time we're going to try and build something self-sufficient we're going to have a child meter class which is completely self-sufficient and self-reliant it should know how to fetch its own values and it should also be responsible for repainting itself previously we had exposed a setter method to set a value for the levels this time we'll instead have a function pointer a pointer to a function when executed will actually give us the level values so we'll call it value supplier and we need the function pointer to be supplied at some point of time we can put that in the constructor so the constructor of the vertical gradient meter will accept a value function this can be an r value reference the double ampersand expresses that it's an r value reference it's usually used during move semantics so essentially we want the value function to be moved into the value supplier it's just a simple way of instantiating something by moving rather than copying we'll get a better idea of what the value supplier is and how we actually pass the value of the function method soon enough till then we'll assume that when whenever we call value supplier we will be getting the latest value of the level after that let's get the local bounds of the component now we'll set the background color of the component the mirror's background color will be black this time after that we need to draw the meter itself we need to create a color gradient now so we'll use the color gradient class in juice it accepts a few arguments the first one is the starting color let's say it starts from green and it starts at the very bottom and then the color flows to red and that could be at the top of the component and we're trying to do a linear gradient and not a radial one so the last argument will be false now this doesn't actually get you yellow in the middle you have to explicitly add a new color the way you do this is by add color method on this instance we want to add a new color halfway between green and red and this new color is yellow instantiating a new color gradient class is well it's a bit expensive it creates and stores a lot of colors underneath since the color gradient doesn't change it can be moved out of the paint method we can try and put it in the constructor but then the bounds aren't available in the constructor the bounds of the component are not set during construction of the component it's set later when you're actually calling set bounds of the component so the best place to instantiate the color gradient would be within the resized method which will be called every time the component dimensions change let's have a private member variable called gradient and let's instantiate this here now within the paint method we set gradient fill on the graphics object of videot with the new gradient that we created now we need to map the level values from its original range of negative 60 to 6 to the new range of 0 to the total height of the component we'll call this scaled y next all we do is fill the rectangle with the bounds and then remove the scaled y amount of it we take a chunk out of the bottom of the bounds with the height as scaled y this will give us the actual meter component that needs to be drawn now since this class manages its own values it could be argued that it should manage its own repainting whenever required so this class could inherit from the timer class and then call start timer head c of 24. we need to overwrite the pure virtual function timer callback and in this method we simply just call the repaint method on the component previously the parent component which is the plugin editor used to do this this meter is truly self-sufficient now all we need to do in the parent create new instance of it and make it visible as before let's include the new class component let's create a couple of new instance variables of the linear gradient meter since the linear gradient meter has a constructor which accepts the the function pointer essentially we need to initialize these instances within the constructor initializer list this is where we pass the function pointer we'll create an anonymous function here using a lambda essentially if you're not too familiar with lambdas they're essentially functions that you don't really have a name for but the syntax can be a little confusing so i really suggest you know getting a primer of it somewhere else we're expecting a function um with a signature which doesn't accept anything but returns a floating point value and within the body of this function we'll just return audio processor dot rms value of the 0th channel and since audio processor is outside the scope of the lambda itself we need to pass it by reference we'll do the same with the right channel meter as well this time we'll get the rms value of the first channel which is the right channel we add and make visible these components i'm going to slightly increase the height of the parent component just to accommodate these new level meters that we are drawing and in the resize method we'll set the bounds of the vertical meters they can be 100 pixels from the left let's say about 200 pixels from the top and a width of 15 and a height of 200 the right channel meter will have something similar just offset the x value by about 20 pixels let's see what it looks like [Music] that's pretty cool we have a solid continuous linear gradient meter essentially now for the cheeky visual decorations on top i've got an image of a grill here it's just like a metallic grill of sort which has holes in the middle and this is a png image and it's transparent as well it's really quite simple to create one of these if you have adobe illustrator or inkscape or any of those vector design softwares you can just create like a big rectangle and make it a little rounded and create tiny rectangles and just duplicate them and select everything and just say subtract whatever is in the front on voila you get meter or whatever looks like it probably put a drop shadow on it and you're done with an acid let's pull this asset into our project i'm going to create a new folder within our project i'll call it assets i'll put the png image inside it i'll drag this folder into the producer project as well and then save it if you've never dealt with images or binary data in producer you will have access to the binary data within your project so let's get back to the class i'll create a new instance of an image class and i'll call it grill now i'll fetch the actual binary data which represents my png file i can get this from image cache get from memory the binary data is embedded within the binary data class as static members the first argument that it accepts is the actual binary data itself the second one is the size of this data in bytes now i could draw this image at the end of the paint method after drawing everything else but since i'm drawing this on top of everything else it sort of makes sense to leverage the paint over children method let's draw the image let's call the draw image method within the graphics object this accepts a drawable or an image which is going to be our grill instance and we're going to fill the entire bounds of the component now let's get back to the resized method of the parent now we've said it's 15 pixels wide here but we kind of need to change it because since we're dealing with raster images we want to try and retain the same size of the raster image so if i inspect the properties of the image it says that it's about 200 pixels in height and 25 pixels wide so let's use those dimensions one last change that i'd like to make is that i can go back into the paint method of the component and over here i'll reduce the size of the actual drawable area of the meter itself the grill occupies the entire component size you want the meter itself to be slightly smaller in area so it fits nicely into the grill and not overflow i think if you've done everything right so far it should be good to go let's see what it looks like that is neat that's pretty much what we were aiming for anyway [Music] let's talk about one more meter today it's this one the discrete meter it's got little bulbs that turn on and off based on the level values you can configure the number of bulbs that you want in your plug-in it's essentially to make a discrete meter widget without the need for drawing raster image on top it can get a little more complicated but again the principle is quite simple so if you observe this meter widget you can see that each bulb is black when it's turned off and then when it's turned on it's got a little black outline and then it's filled by a solid color color depends on where the position of the bulb is in the meter and it's got this slight glow an outer glow effect so that's what we need to implement as usual let's create a new file i'm going to call this vertical discrete mirror let's save the project i'm going to copy the contents of the the previous meter that we just made the linear gradient meter we're going to use this as the base and we're going to start from there i'll rename the class the vertical discrete meter i'm going to get rid of things that i don't need in this namely the image and i'm also going to get rid of the paint over children method i'll clear out most of the paint method as well because we'll start from scratch there we'll hold on to the linear gradient object that we created earlier and we'll continue to use the value supplier method of getting the value and making this entire class self-sufficient as the other one that we just did now for the bulbs themselves let's make a class to represent each bulb object i'll call this class bulb it'll extend component the state of the bulb can either be on or off i'll maintain a member variable for the state i'll call it is on i'll set it to false initially and i'm going to expose a setter method here to set the state of the ball the color of each bulb doesn't change over time so what we can do is create a member variable for storing the color and we can get the color value during construction i'm going to overwrite the paint method within the bulb class within the paint method we're going to get the bones first the bounds of each bulb is set within the meter class itself so we'll we'll assume that some amount of area is allocated for each bulb since we also want to represent a glow let's reduce the size of the bounds of the actual bulb itself and we'll reduce it by an amount of 4 pixels each side we might need to reference this 4 pixels later so we'll pull it out and we'll call it delta so to draw the basic shape of the bulb we will draw a circle within this bounds bounce now if the sides if the width and the height of the bounds are exactly the same meaning if bounds is a square then it's all well and good we can just inscribe an ellipse within that bounce and we would end up getting a perfect circle but let's say the bounds isn't a perfect square let's say it's a rectangle where the width and the height are not the same if we were to draw an ellipse in within that bound we wouldn't get a perfect circle to avoid that problem we'll just get the smallest side and make sure the bounds that we're drawing the circle in is a square j min will give us a minimum of two values so we'll query j min of width and height and this will be the side of our bounds now let's create the actual bounds that will fill up the bulb and this will be a rectangle of the same x and y as before with the width and height as side inside if the bulb is on we'll set the color to be the actual color that was provided to us during construction if not we'll color it black to actually draw the ellipse we'll use fill ellipse from the graphics class after that i'll put an outline stroke on the bulb i'll set the color to be black i'll use the draw ellipse method to draw the outline stroke i'll set the stroke width to be 1 pixel now if the bulb is on we also want to draw the glow as well the glow that's emitted by the bulb the way we're going to achieve the glow effect is to have a radial gradient whose opacity decreases over distance we'll use the color gradient class again the starting color will be the same color of the bulb but with a reduced alpha alpha here is just opacity i'll set the alpha to be 0.3 this color is going to start from the center of the bounds and towards the edge of the bounce the color will fade away with its alpha being zero but also i'll increase the lightness of the color so that it has more whites the second point here is sort of pointless you don't really need it if you're setting radial gradient to be true the last argument is radial gradient so i'm going to set that to true now you you can pull radial this radial gradient out into the constructor or into the resize method like we did previously because nothing really has to change here but i'm going to leave it up to you guys to do it now to apply the glow effect we'll fill the ellipse with this new gradient color we'll use the bulb fill bounds if you remember earlier we reduced we constrained the size of the bulb bounds by an amount of delta now the glow effect needs to protrude outside the bounds of the bulb itself so for that we'll expand the bulb bounce to include the extra 4 pixels on each side and that's the bulb class done now to actually use the verb class within the vertical discrete meter class we can hold on to a collection of bulbs here in the main class a good data structure to go with is a standard vector so a step vector of bulb will be the way to go but we can't actually do this if we try to do this we'll end up getting plenty of compilation errors the reason is that a juice component is not copyable and it's not movable and you can't have non-copyable items within a vector because when the vector resizes or when the vector components are added it needs to be either copied or moved and juice components allow neither and one of the workarounds for that and you'll come across this quite often when you're dealing with a collection of components you'll have to use a vector of unique pointers of the component so we'll use a standard vector and a standard unique pointer of bulb and we'll call the collection bulbs we'll also define a total number of bulbs that we want to use i'm just going to use a constant integer here of 10 but of course this can be configurable during construction or dynamically resized as well if you want it to be let's assign some space for these bulbs so in the resize method i'm going to get the local bounce first so i'm going to chop off parts of the bulb bounce and assign it to each one of the bulbs so let's figure out the bulb height first the height of each bulb is essentially going to be the total height of the component divided by the total number of bulbs so that will give you the height of each ball now in the resize method i'm going to clear all the bulbs from the collection and re-add them over here now we need to iterate through the collection of bulbs we could use a for each loop here but the positional index is kind of needed in one of the places so we'll use this standard old-timey way of uh writing a for loop for i equal to zero i is less than the total number of bulbs and i plus plus we create a new bulb instance here and since it needs to be a unique pointer uh we'll say stood make unique of a bulb and the constructor of the bulb accepts a color if you remember how do we get the right color starting from the bottom the color of the bulbs will be green going up to yellow and then to red since these are discrete bulbs and there are only about 10 of them we want to get a distinct color from the gradient so i'm going to say get colored position from the gradient a position is a value and the position has to be a value between 0 and 1 so i'm going to say i divided by total number of bulbs that will give us a value between 0 and 1. when the color is at the bottom it'll give us a value of zero the colors at the top will give us a value of nine over ten never really get red but you know that's that's how it is once we've created a unique pointer of the bow let's add and make visible this component and then i'll set the bulb bounds and while i'm setting the bounds of the bulb here so i'm going to take the bulb bounce rectangle that we created and i'm going to chop off starting from the bottom the height of the bulb as this loop iterates more and more chunks are chopped off from the bottom of this bulb bounce rectangle and finally we push this newly created bulb into the collection and just for efficiency sake we will move it instead of copying the unique pointer now the last thing that we need to do in this class is to essentially activate and deactivate the bulb as necessary based on the level value the level value is available to us in the paint method now let's make a decision for each one of the bulbs so let's iterate through the collection of bulbs that we have so if the level value is greater than i plus 1 or index plus 1 divided by the total number of bulbs will set the state of the bulb at that index to be true otherwise it'll be false the idea here is let's say for example the level is 0.25 this would mean the first two bulbs at the bottom would be activated but not the third one these four lines can essentially be written in one line like this but it's sort of cumbersome and hard to read so i'll leave the judgment up to you regarding which style you'd rather use one more thing i forgot and like coming back to this after a while is the level we assumed that it'd be from zero to one but actually the value supplier will give the level in decibels we need to map the decibel values back into the original range of gain so we want to say map the level value given by the supplier from its original range of negative 60 to 6 decibels to the new range of zero to one that's perfect now let's go back to the plugin editor and we can include the new file here include component slash vertical discretemeter.hedge or create new instance variables as before of the vertical discrete meter and in the constructor we know the drill we'll initialize them in the initializer list and we'll provide the value supplier function and the constructor will add and make visible these components and in the resize method of the plugin editor we'll assign some space to these new mirrors so we'll set the left discrete meters bounce as two more pixels x and 200 pixels y uh 25 pixels in width and 200 pixels in height when we'll use pretty much the same dimensions for the right meter we'll translate horizontally by 30 pixels i think we should have everything so let's try build it [Music] well there you go if you've got any questions or problems hit me up over here and i'll try to answer them all the code is on github and the link is in the description below so you can check it out there also let me know your thoughts about the format of this video it's quite tightly edited and i've sped up the video as well so that you don't have to sit and watch me type well i've got more ideas in what to include in the next visualizations and juice series but if you've got any suggestions hit me up here until next time then you
Info
Channel: Akash Murthy
Views: 1,087
Rating: undefined out of 5
Keywords:
Id: ILMdPjFQ9ps
Channel Id: undefined
Length: 54min 44sec (3284 seconds)
Published: Mon Sep 13 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.