I want to make a waveform drawing for an audio recorder in Android. The usual one with lines/bars, like this one:
More importantly, I want it live, while the song is being recorded. My app already computes the RMS through AudioRecord. But I am not sure which is the best approach for the actual drawing in terms of processing, resources, battery, etc.
The Visualizer does not show anything meaningful, IMO (are those graphs more or less random stuff??).
I've seen the canvas approach and the layout approach (there are probably more?). In the layout approach you add thin vertical layouts in a horizontal layout. The advantage is that you don't need to redraw the whole thing each 1/n secs, you just add one layout each 1/n secs... but you need hundreds of layouts (depending on n). In the canvas layout, you need to redraw the whole thing (right??) n times per second. Some even create bitmaps for each drawing...
So, which is cheaper, and why? Is there anything better nowadays? How much frequency update (i.e., n) is too much for generic low end devices?
EDIT1
Thanks to the beautiful trick #cactustictacs taught me in his answer, I was able to implement this with ease. Yet, the image is strangely rendered kind of "blurry by movement":
The waveform runs from right to left. You can easily see the blur movement, and the left-most and right-most pixels get "contaminated" by the other end. I guess I can just cut both extremes...
This renders better if I make my Bitmap bigger (i.e., making widthBitmap bigger), but then the onDraw will be heavier...
This is my full code:
package com.floritfoto.apps.ave;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import java.util.Arrays;
public class Waveform extends androidx.appcompat.widget.AppCompatImageView {
//private float lastPosition = 0.5f; // 0.5 for drawLine method, 0 for the others
private int lastPosition = 0;
private final int widthBitmap = 50;
private final int heightBitmap = 80;
private final int[] transpixels = new int[heightBitmap];
private final int[] whitepixels = new int[heightBitmap];
//private float top, bot; // float for drawLine method, int for the others
private int aux, top;
//private float lpf;
private int width = widthBitmap;
private float proportionW = (float) (width/widthBitmap);
Boolean firstLoopIsFinished = false;
Bitmap MyBitmap = Bitmap.createBitmap(widthBitmap, heightBitmap, Bitmap.Config.ARGB_8888);
//Canvas canvasB = new Canvas(MyBitmap);
Paint MyPaint = new Paint();
Paint MyPaintTrans = new Paint();
Rect rectLbit, rectRbit, rectLdest, rectRdest;
public Waveform(Context context, AttributeSet attrs) {
super(context, attrs);
MyPaint.setColor(0xffFFFFFF);
MyPaint.setStrokeWidth(1);
MyPaintTrans.setColor(0xFF202020);
MyPaintTrans.setStrokeWidth(1);
Arrays.fill(transpixels, 0xFF202020);
Arrays.fill(whitepixels, 0xFFFFFFFF);
}
public void drawNewBar() {
// For drawRect or drawLine
/*
top = ((1.0f - Register.tone) * heightBitmap / 2.0f);
bot = ((1.0f + Register.tone) * heightBitmap / 2.0f);
// Using drawRect
//if (firstLoopIsFinished) canvasB.drawRect(lastPosition, 0, lastPosition+1, heightBitmap, MyPaintTrans); // Delete last stuff
//canvasB.drawRect(lastPosition, top, lastPosition+1, bot, MyPaint);
// Using drawLine
if (firstLoopIsFinished) canvasB.drawLine(lastPosition, 0, lastPosition, heightBitmap, MyPaintTrans); // Delete previous stuff
canvasB.drawLine(lastPosition ,top, lastPosition, bot, MyPaint);
*/
// Using setPixel (no tiene sentido, mucho mejor setPixels.
/*
int top = (int) ((1.0f - Register.tone) * heightBitmap / 2.0f);
int bot = (int) ((1.0f + Register.tone) * heightBitmap / 2.0f);
if (firstLoopIsFinished) {
for (int i = 0; i < top; ++i) {
MyBitmap.setPixel(lastPosition, i, 0xFF202020);
MyBitmap.setPixel(lastPosition, heightBitmap - i-1, 0xFF202020);
}
}
for (int i = top ; i < bot ; ++i) {
MyBitmap.setPixel(lastPosition,i,0xffFFFFFF);
}
//System.out.println("############## "+top+" "+bot);
*/
// Using setPixels. Works!!
top = (int) ((1.0f - Register.tone) * heightBitmap / 2.0f);
if (firstLoopIsFinished)
MyBitmap.setPixels(transpixels,0,1,lastPosition,0,1,heightBitmap);
MyBitmap.setPixels(whitepixels, top,1, lastPosition, top,1,heightBitmap-2*top);
lastPosition++;
aux = (int) (width - proportionW * (lastPosition));
rectLbit.right = lastPosition;
rectRbit.left = lastPosition;
rectLdest.right = aux;
rectRdest.left = aux;
if (lastPosition >= widthBitmap) { firstLoopIsFinished = true; lastPosition = 0; }
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
width = w;
proportionW = (float) width/widthBitmap;
rectLbit = new Rect(0, 0, widthBitmap, heightBitmap);
rectRbit = new Rect(0, 0, widthBitmap, heightBitmap);
rectLdest = new Rect(0, 0, width, h);
rectRdest = new Rect(0, 0, width, h);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawNewBar();
canvas.drawBitmap(MyBitmap, rectLbit, rectRdest, MyPaint);
canvas.drawBitmap(MyBitmap, rectRbit, rectLdest, MyPaint);
}
}
EDIT2
I was able to prevent the blurring just using null as Paint in the canvas.drawBitmap:
canvas.drawBitmap(MyBitmap, rectLbit, rectRdest, null);
canvas.drawBitmap(MyBitmap, rectRbit, rectLdest, null);
No Paints needed.
Your basic custom view approach would be to implement onDraw and redraw your current data each frame. You'd probably keep some kind of circular Buffer holding your most recent n amplitude values, so each frame you'd iterate over those, and use drawRect to draw the bars (you'd calculate things like width, height scaling, start and end positions etc in onSizeChanged, and use those values when defining the coordinates for the Rects).
That in itself might be fine! The only way you can really tell how expensive draw calls are is to benchmark them, so you could try this approach out and see how it goes. Profile it to see how much time it takes, how much the CPU spikes etc.
There are a few things you can do to make onDraw as efficient as possible, mostly things like avoiding object allocations - so watch out for loop functions that create Iterators, and in the same way you're supposed to create a Paint once instead of creating them over and over in onDraw, you could reuse a single Rect object by setting its coordinates for each bar you need to draw.
Another approach you could try is creating a working Bitmap in your custom view, which you control, and calling drawBitmap inside onDraw to paint it onto the Canvas. That should be a pretty inexpensive call, and it can easily be stretched as required to fit the view.
The idea there, is that very time you get new data, you paint it onto the bitmap. Because of how your waveform looks (like blocks), and the fact you can scale it up, really all you need is a single vertical line of pixels for each value, right? So as the data comes in, you paint an extra line onto your already-existing bitmap, adding to the image. Instead of painting the entire waveform block by block every frame, you're just adding the new blocks.
The complication there is when you "fill" the bitmap - now you have to "shift" all the pixels to the left, dropping the oldest ones on the left side, so you can draw the new ones on the right. So you'll need a way to do that!
Another approach would be something similar to the circular buffer idea. If you don't know what that is, the idea is you take a normal buffer with a start and an end, but you treat one of the indices as your data's start point, wrap around to 0 when you hit the last index of the buffer, and stop when you hit the index you're calling your end point:
Partially filled buffer:
|start
123400
|end
Data: 1234
Full buffer:
|start
123456
|end
Data: 123456
After adding one more item:
|start
723456
|end
Data: 234567
See how once it's full, you shift the start and end one step "right", wrapping around if necessary? So you always have the most recent 6 values added. You just have to handle reading from the correct index ranges, from start -> lastIndex and then firstIndex -> end
You could do the same thing with a bitmap - start "filling" it from the left, increasing end so you can draw the next vertical line. Once it's full, start filling from the left by moving end there. When you actually draw the bitmap, instead of drawing the whole thing as-is (723456) you draw it in two parts (23456 then 7). Make sense? When you draw a bitmap to the canvas, there's a call that takes a source Rect and a destination one, so you can draw it in two chunks.
You could always redraw the bitmap from scratch each frame (clear it and draw the vertical lines), so you're basically redrawing your whole data buffer each time. Probably still faster than the drawRect approach for each value, but honestly not much easier than the "treat the bitmap as another circular buffer" method. If you're already managing one circular buffer, it's not much more work - since the buffer and the bitmap will have the same number of values (horizontal pixels in the bitmap's case) you can use the same start and end values for both
You would never do this with layouts. Layouts are for premade components. They're high level combinations of components and you don't want to dynamically add or remove views from it frequently. For this, you use a custom view with a canvas. Layouts aren't even an option for something like this.
I'm new to android studio and I'm learning a lot right now. But I'm stuck now, I want to create something like a container for a 16x7 boardgame with individual fields. I think theres better ways to do this and I'm trying around since 2 hours, with no success. Right now I'm using a Grid View to display my String[] with an array adapter to a simple selectable list item, but it doesn't scale right and I need to scroll down to see all of the fields, which is bad for a videogame.
Tl,dr: I'd like to display an array of 112 elements like a boardgame, what container can I use to create those individual fields, maybe even set the amounts of rows(7) and columns(16), and display it simple like an actual boardgame?
I would render bitmap tiles to the Canvas. Below code is just an outline:
// You will set this frameBuffer to an ImageView in the app's main screen.
// You should set the ImageView's ScaleType properly.
Bitmap frameBuffer = Bitmap.createBitmap(
TILE_SIZE * 16, TILE_SIZE * 7, Bitmap.Config.ARGB_8888
);
Canvas canvas = new Canvas(frameBuffer);
Paint paint = new Paint();
for (int y = 0; y < 7; y++) {
for (int x = 0; x < 16; x++) {
// You are supposed to define bitmapTiles.get() method elsewhere.
canvas.drawBitmap(
bitmapTiles.get(x, y), TILE_SIZE * x, TILE_SIZE * y, paint
);
}
}
I have made some simple benchmarks for my game.
It turns out the average time spent in my updateGame() function is: 1-4 ms
Average time spent in my renderGame() function: 15-17ms.
I use a SurfaceView and a dirty rect. However the size of the dirty rect does not seem to affect performance which is strange.
Anyhow, the renderGame() function is the bottleneck here and keeps me from achieveing the 45-60 FPS I need.
I also use a Samsung S4 Mini so it is a fairly powerful phone so bottom end phones it will be even worse.
My render function looks like this:
#Override
public void render( double interpolation, Canvas canvas )
{
canvas.drawColor( Color.TRANSPARENT, PorterDuff.Mode.CLEAR );
canvas.drawBitmap( background, null, backgroundRect, null );
for ( int i = 0; i < theGameEdges.length; i++ )
{
theGameEdges[ i ].draw( canvas );
}
for ( int i = 0; i < obstacles.length; i++ )
{
obstacles[ i ].draw( canvas );
}
for ( int i = 0; i < holes.length; i++ )
{
holes[ i ].draw( canvas );
}
for ( int i = 0; i < goalFlags.length; i++ )
{
goalFlags[ i ].draw( canvas );
}
balls = level.getBallPositions();
for ( int i = 0; i < balls.length; i++ )
{
SilverBallRect[ i ].set(
balls[ i ].X + theOffsetX,
balls[ i ].Y + theOffsetTopY,
balls[ i ].X + ballSize + theOffsetX,
balls[ i ].Y + ballSize + theOffsetTopY
);
canvas.drawBitmap( SilverBall, null, SilverBallRect[ i ], null );
}
stringBuilder.setLength( 0 );
stringBuilder.append( timeString ).append( engine.getTimeSeconds() );
canvas.drawText( stringBuilder, 0, stringBuilder.length(), textX, textY, theTextPaint );
}
Could I improve the performance here by using OpenGL?
Can I have different layers in the view or something to improve performance? Don't know how that would work since it is all pixels in the end. but...
Any other hints on performance?
The size of the dirty rect affects the part of the screen that is updated, not the part that is rendered -- it's not a clip rect. If you draw pixels outside the dirty rect, they will be drawn, but will not be overwritten when the previous frame is copied into the outside areas.
To get a performance improvement, your code needs to touch only the pixels inside the rect. Otherwise the areas outside the rect are effectively being drawn twice.
Canvas rendering to a SurfaceView surface is never hardware-accelerated. You will almost certainly improve performance by using OpenGL ES. You will also need to learn OpenGL ES, which isn't entirely trivial.
If you want hardware-accelerated Canvas rendering, you can use a custom View instead of a SurfaceView surface.
Update: for software rendering it's even more important to keep the updated pixel count low. Consider rendering at a lower resolution and upscaling. See this article and the "hardware scaler exerciser" in Grafika.
For more details about the Android graphics architecture, see this document.
I'm writing a scratch card like app, and I use a SurfaceView for that.
I fill it with some kind of color and I draw some Path on it with PorterDuff.Mode.CLEAR PorterDuffXfermode. I have to identify when the user fully scratched it (the SurfaceView's canvas is fully transparent). Can anybody give me some advice, how to identify it?
I tried it with saving the coordinates of the paths, but because of the drawing stroke width I can't calculate the covered area well.
I tried to get a Bitmap from the SurfaceView's getDrawingCache method and iterate on its pixels and use the getPixel method. It doesn't work and i think it would be not an efficient way to examine the canvas.
Assuming the canvas will not be large or scalable to an arbitrary size I think looping over the pixels would be effective.
Given a canvas of large or arbitrary size I would create an array representation of the canvas and mark pixels as you go, keeping a count of how many the user has hit at least once. Then test that number against a threshold value that determines how much of the ticket must be scratched for it to be considered "scratched off". Incoming pseudo-code
const int count = size_x * size_y; // pixel count
const int threshhold = 0.8 * count // user must hit 80% of the pixels to uncover
const int finger_radius = 2; // the radias of our finger in pixels
int scratched_pixels = 0;
bit [size_x][size_y] pixels_hit; // array of pixels all initialized to 0
void OnMouseDown(int pos_x, int pos_y)
{
// calculates the mouse position in the canvas
int canvas_pos_x, canvas_pos_y = MousePosToCanvasPos(pos_x, pos_y);
for(int x = canvas_pos_x - finger_rad; x < canvas_pos_x + brush_rad; ++x)
{
for(int y = canvas_pos_y - finger_rad; y < canvas_pos_y + brush_rad; ++y)
{
int dist_x = x - canvas_pos_x;
int dist_y = y - canvas_pos_y;
if((dist_x * dist_x + dist_y * dist_y) <= brush_rad * brush_rad
&& pixels_hit[x][y] == 0)
{
++scratched_pixels;
pixels_hit[x][y] = 1;
}
}
}
}
bool IsScratched()
{
if(scratched_pixels > threshhold)
return true;
else
return false;
}
So I have a bitmap that I have loaded from a resource file (an PNG image):
Bitmap map = BitmapFactory.decodeResource(getResources(), R.drawable.wave);
If I draw this bitmap only once using canvas.drawBitmap(...); then there is no problem. However, If I draw that very same bitmap multiple times, then the picture keeps flashing back and forth, not steady like before.
I suspected that I cannot use the same bitmap more than once so I tried to load the image into a new bitmap every time when I want to draw the same picture, but it does not help, the behavior still persists.
The program is complicated, but basically, I want to draw a ocean wave. I have a image of a small wave. To make the effect of the wave moving from the left edge of the screen to the right edge. I keep track of the position of the left edge of the bitmap.
// The ocean.
private ArrayList<Wave> waves;
// Draw the waves and update their positions.
for (int i = 0; i < this.waves.size(); i++)
{
Wave wave = this.waves.get(i);
// Go through each of the sub-waves of this current wave.
for (int j = 0; j < wave.getSubWaveEdges().size(); j++)
{
// Get the sub wave.
final float subWaveEdge = wave.getSubWaveEdges().get(j);
canvas.drawBitmap( wave.getSubWave(j), subWaveEdge, 40, brush);
wave.setSubWaveEdge(j, subWaveEdge + (float) 0.5);
}
// Update this current wave.
wave.update();
// If the wave has passed the left edge of the screen then add a new sub-wave.
if (wave.getFarthestEdge() >= 0)
wave.addSubWaveEdges(wave.getFarthestEdge() - this.getWidth());
}
If the left edge of a bitmap is inside the screen then I create a new bitmap from the same image file and draw. Here is the class Wave:
private class Wave
{
private Bitmap wave;
private float farthestEdge;
private ArrayList<Float> subWaveEdges;
private ArrayList<Bitmap> subWaves;
public Wave(Bitmap wave)
{
this.wave = wave;
this.farthestEdge = 0;
this.subWaveEdges = new ArrayList<Float>();
this.subWaves = new ArrayList<Bitmap>();
}
public Bitmap getWave ()
{ return this.wave; }
public void setWave (Bitmap wave)
{ this.wave = wave; }
public float getFarthestEdge ()
{ return this.farthestEdge; }
public void setFarthestEdge (final float furthestEdge)
{ this.farthestEdge = furthestEdge; }
public ArrayList<Float> getSubWaveEdges ()
{ return subWaveEdges; }
public void setSubWaveEdge (final int index, final float value)
{
this.subWaveEdges.remove(index);
this.subWaveEdges.add(value);
}
public void addSubWaveEdges (final float edge)
{
this.subWaveEdges.add(edge);
Bitmap newSubWave = BitmapFactory.decodeResource(getResources(), R.drawable.wave);
newSubWave = Bitmap.createScaledBitmap(newSubWave, MasterView.this.getWidth(), newSubWave.getHeight(), true);
this.subWaves.add(newSubWave);
}
public Bitmap getSubWave(final int index)
{ return this.subWaves.get(index); }
public void update ()
{
// Check to see if there is any sub-wave going outside of the screen.
// If there is then remove that wave.
for (int index = 0; index < this.subWaveEdges.size(); index++)
if (this.subWaveEdges.get(index) > MasterView.this.getWidth())
{
this.subWaveEdges.remove(index);
this.subWaves.remove(index);
}
// Set the farthest edge to the other side of the screen.
this.farthestEdge = MasterView.this.getWidth();
// Get the farthest edge of the wave.
for (int index = 0; index < this.subWaveEdges.size(); index++)
if (this.subWaveEdges.get(index) < this.farthestEdge)
this.farthestEdge = this.subWaveEdges.get(index);
}
}
Another suspicion that I have is that may be when I create two bitmaps from the same resource file, the pixels of the image are divided among two bitmaps, meaning that each bitmap only gets part of the pixels, not all. I am suspecting this because when the bitmaps are drawn, the parts where they overlaps are drawn steadily, no flashing.
Anyone has stumbled upon this problem and know how to fix?
Thanks,
Viktor Lannér, Thank you for helping, but I don't think that's the problem. I understand it is hard to read my codes since it is only a small piece of the big program.
However, I found the problem: This is not mentioned in my original question, but in order to simulate the two waves moving after one another, I have to draw the next wave as soon as the first wave enters the screen. However, each wave is longer than the width of the screen. Therefore, I have to draw the next wave from "outside" the screen if you know what I mean. It means that the next wave is drawn from a negative x-coordinate from outside the screen:
// If the wave has passed the left edge of the screen then add a new sub-wave.
if (wave.getFarthestEdge() >= 0)
wave.addSubWaveEdges(wave.getFarthestEdge() - this.getWidth());
And I found out that it does not like this. This is what causes the flashing back and forth.
In order to fix this, instead of drawing the next wave from outside the screen, I use this method:
canvas.drawBitmap (Bitmap bitmap, Rect source, Rect destination, Paint paint)
This method allows you to specify a rectangular region on the bitmap to be drawn to the screen and a rectangular region on the screen where that part of the bitmap will be drawn over. I use this method to draw the next wave. As the next wave moves into the screen, I change the "source" and "destination" appropriately to draw parts of the bitmap.
I just wanted to say that I had an issue where the images on my canvas were flashing back and forth, or, flashing between black and my first frame until I made a movement, almost as if the canvas was rapidly switching between its current and last image.
This might have had something to do with your situation, and to fix it I found out that it was because I was locking the canvas every frame, even when I had nothing to draw. For whatever reason, that lock, I think, created this situation.
I got around it by doing something like this:
if (needToRedraw == true) {
canvas = mSurfaceHolder.lockCanvas(null);
... logic to eventually draw on that canvas ...
}
Before canvas.drawBitmap(...) call; try to use canvas.drawColor(Color.BLACK) to clear the Canvas from previous drawings.
Sample code:
// Stuff.
canvas.drawColor(Color.BLACK);
canvas.drawBitmap(wave.getSubWave(j), subWaveEdge, 40, brush);
// Stuff.