001/******************************************************************************* 002 * Copyright (c) 2024, 2026, Olivier Ayache. All rights reserved. 003 * 004 * This file is part of AVPKit. 005 * 006 * AVPKit is free software: you can redistribute it and/or modify 007 * it under the terms of the GNU Lesser General Public License as published by 008 * the Free Software Foundation, either version 3 of the License, or 009 * (at your option) any later version. 010 * 011 * AVPKit is distributed in the hope that it will be useful, 012 * but WITHOUT ANY WARRANTY; without even the implied warranty of 013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 014 * GNU Lesser General Public License for more details. 015 * 016 * You should have received a copy of the GNU Lesser General Public License 017 * along with AVPKit. If not, see <http://www.gnu.org/licenses/>. 018 *******************************************************************************/ 019 020package com.avpkit.mediatool.demos; 021 022import java.awt.Color; 023import java.awt.Graphics2D; 024import java.awt.geom.Ellipse2D; 025import java.awt.RenderingHints; 026import java.awt.image.BufferedImage; 027 028import java.util.Random; 029import java.util.Vector; 030import java.util.Collection; 031 032import static java.util.concurrent.TimeUnit.SECONDS; 033import static com.avpkit.core.Global.DEFAULT_TIME_UNIT; 034 035/** 036 * An implementation of {@link Balls} that moves the balls 037 * around in a rectangle, and plays a sound for each 038 * ball that changes when it hits a wall. 039 * 040 * @author trebor 041 * 042 */ 043public class MovingBalls implements Balls 044{ 045 // the balls 046 047 private final Collection<Ball> mBalls; 048 049 // a place to store frame images 050 051 private final BufferedImage mImage; 052 053 // the graphics for the image 054 055 private final Graphics2D mGraphics; 056 057 // a place to put the audio samples 058 059 private final short[] mSamples; 060 061 /** 062 * Grow a set of balls. 063 */ 064 065 public MovingBalls(int ballCount, int width, int height, int sampleCount) 066 { 067 // create the balls 068 069 mBalls = new Vector<Ball>(); 070 while (mBalls.size() < ballCount) 071 mBalls.add(new Ball(width, height)); 072 073 // create a place to put images 074 075 mImage = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR); 076 077 // create graphics for the images 078 079 mGraphics = mImage.createGraphics(); 080 mGraphics.setRenderingHint( 081 RenderingHints.KEY_ANTIALIASING, 082 RenderingHints.VALUE_ANTIALIAS_ON); 083 084 // create a place to put the audio samples 085 086 mSamples = new short[sampleCount]; 087 } 088 089 /* (non-Javadoc) 090 * @see com.avpkit.mediatool.demos.Balls#getVideoFrame(long) 091 */ 092 093 public BufferedImage getVideoFrame(long elapsedTime) 094 { 095 // clear the frame 096 097 mGraphics.setColor(Color.WHITE); 098 mGraphics.fillRect(0, 0, mImage.getWidth(), mImage.getHeight()); 099 100 // update and paint the balls 101 102 for (Ball ball: mBalls) 103 { 104 ball.update(elapsedTime); 105 ball.paint(mGraphics); 106 } 107 108 // return the frame image 109 110 return mImage; 111 } 112 113 /* (non-Javadoc) 114 * @see com.avpkit.mediatool.demos.Balls#getAudioFrame(int) 115 */ 116 117 public short[] getAudioFrame(int sampleRate) 118 { 119 // zero out the audio 120 121 for (int i = 0; i < mSamples.length; ++i) 122 mSamples[i] = 0; 123 124 // add audio for each ball 125 126 for (Ball ball: mBalls) 127 ball.setAudioProgress(addSignal(ball.getFrequency(), sampleRate, 128 1d / mBalls.size(), ball.getAudioProgress(), mSamples)); 129 130 // return new audio samples 131 132 return mSamples; 133 } 134 135 /** 136 * Add a signal of a given frequency to a set of audio samples. If 137 * the total signal value exceeds the percision of the samples, the 138 * signal is clipped. 139 * 140 * @param frequency the frequency of the signal to add 141 * @param sampleRate the number samples in a second 142 * @param volume the amplitude of the signal 143 * @param progress the start position the signal, initally should be 144 * zero, it will be updated by addSignal and returned, pass the 145 * returned value into subsquent calls to addSignal 146 * @param samples the array to which the samples will be added 147 * 148 * @return the progress at the end of the sample period 149 */ 150 151 public static double addSignal(int frequency, int sampleRate, double volume, 152 double progress, short[] samples) 153 { 154 final double amplitude = Short.MAX_VALUE * volume; 155 final double epsilon = ((Math.PI * 2) * frequency) / sampleRate; 156 157 // for each sample 158 159 for (int i = 0; i < samples.length; ++i) 160 { 161 // compute sample as int 162 163 int sample = samples[i] + (short)(amplitude * Math.sin(progress)); 164 165 // clip if nedded 166 167 sample = Math.max(Short.MIN_VALUE, sample); 168 sample = Math.min(Short.MAX_VALUE, sample); 169 170 // insert new sample 171 172 samples[i] = (short)sample; 173 174 // update progress 175 176 progress += epsilon; 177 } 178 179 // return progress 180 181 return progress; 182 } 183 184 /** A ball that bounces around inside a bounding box. */ 185 186 static class Ball extends Ellipse2D.Double 187 { 188 public static final long serialVersionUID = 0; 189 190 private static final int MIN_FREQ_HZ = 220; 191 private static final int MAX_FREQ_HZ = 880; 192 193 // ball parameters 194 195 private final int mWidth; 196 private final int mHeight; 197 private final int mRadius; 198 private final double mSpeed; 199 private static final Random rnd = new Random(); 200 private double mAngle = 0; 201 private Color mColor = Color.BLUE; 202 private double mAudioProgress = 0; 203 204 /** Construct a ball. 205 * 206 * @param width width of ball bounding box 207 * @param height height of ball bounding box 208 */ 209 210 public Ball(int width, int height) 211 { 212 mWidth = width; 213 mHeight = height; 214 215 // set a random radius 216 217 mRadius = rnd.nextInt(10) + 10; 218 219 // set the speed 220 221 mSpeed = (rnd.nextInt(200) + 100d) / DEFAULT_TIME_UNIT.convert(1, SECONDS); 222 223 // start in the middle 224 225 setLocation( 226 (mWidth - 2 * mRadius) / 2, 227 (mHeight - 2 * mRadius) / 2); 228 229 // set random angle 230 231 mAngle = rnd.nextDouble() * Math.PI * 2; 232 233 // set random color 234 235 mColor = new Color( 236 rnd.nextInt(256), 237 rnd.nextInt(256), 238 rnd.nextInt(256)); 239 } 240 241 // set the auido progress 242 243 public void setAudioProgress(double audioProgress) 244 { 245 mAudioProgress = audioProgress; 246 } 247 248 // get audio progress 249 250 public double getAudioProgress() 251 { 252 return mAudioProgress; 253 } 254 255 // set ball location 256 257 private void setLocation(double x, double y) 258 { 259 setFrame(x, y, 2 * mRadius, 2 * mRadius); 260 } 261 262 /** 263 * The current frequency of this ball. 264 */ 265 266 public int getFrequency() 267 { 268 double angle = (Math.toDegrees(mAngle) % 360 + 360) % 360; 269 return (int)(angle / 360 * (MAX_FREQ_HZ - MIN_FREQ_HZ) + MIN_FREQ_HZ); 270 } 271 272 /** 273 * Update the state of the ball. 274 * 275 * @param elapsedTime the time which has elapsed since the last update 276 */ 277 278 public void update(long elapsedTime) 279 { 280 double x = getX() + Math.cos(mAngle) * mSpeed * elapsedTime; 281 double y = getY() + Math.sin(mAngle) * mSpeed * elapsedTime; 282 283 if (x < 0 || x > mWidth - mRadius * 2) 284 { 285 mAngle = Math.PI - mAngle; 286 x = getX(); 287 } 288 289 if (y < 0 || y > mHeight - mRadius * 2) 290 { 291 mAngle = -mAngle; 292 y = getY(); 293 } 294 295 setLocation(x, y); 296 } 297 298 /** Paint the ball onto a graphics canvas 299 * 300 * @param g the graphics area to paint onto 301 */ 302 303 public void paint(Graphics2D g) 304 { 305 g.setColor(mColor); 306 g.fill(this); 307 } 308 } 309}