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}