source: trunk/src/org/apollo/audio/SampledTrackModel.java@ 1559

Last change on this file since 1559 was 1559, checked in by davidb, 3 years ago

Beat tracking form mozart's laptop branch merged in

File size: 10.2 KB
Line 
1package org.apollo.audio;
2
3import java.io.IOException;
4import java.util.ArrayList;
5import java.util.List;
6
7import javax.sound.sampled.AudioFormat;
8import javax.sound.sampled.UnsupportedAudioFileException;
9
10import org.apollo.io.AudioIO;
11import org.apollo.mvc.AbstractSubject;
12import org.apollo.mvc.SubjectChangedEvent;
13
14import be.tarsos.dsp.AudioDispatcher;
15import be.tarsos.dsp.beatroot.BeatRootOnsetEventHandler;
16import be.tarsos.dsp.io.jvm.AudioDispatcherFactory;
17import be.tarsos.dsp.onsets.ComplexOnsetDetector;
18import be.tarsos.dsp.onsets.OnsetHandler;
19
20
21/**
22 * A modifiable sampled audio track.
23 *
24 * @author Brook Novak
25 *
26 */
27public class SampledTrackModel extends AbstractSubject implements OnsetHandler {
28
29 private AudioFormat audioFormat = null;
30
31 private byte[] audioBytes = null;
32
33 private int selectionStart = 0; // in frames
34
35 private int selectionLength = -1; // in frames <= 1 not ranged.
36
37 private boolean isAudioModified = false;
38
39 private String currentFilepath = null;
40
41 private String localFilename = null; // never null, immutable
42
43 private String name = null;
44
45 private List<Double> beats;
46
47
48 /**
49 * Constructor.
50 *
51 * @param audioBytes
52 * Pure audio samples. Must not be an empty array.
53 *
54 * @param audioFormat
55 * The format of the given audio samples.
56 *
57 * @param localFilename
58 * The localfilename to associate this track with... imutable. Must never
59 * be null.
60 *
61 * @throws IllegalArgumentException
62 * If audioFormat requires conversion. See SampledAudioManager.isFormatSupportedForPlayback
63 *
64 * @throws NullPointerException
65 * If audioBytes or audioFormat is null
66 */
67 public SampledTrackModel(byte[] audioBytes, AudioFormat audioFormat, String localFilename) {
68
69 if (audioBytes == null)
70 throw new NullPointerException("audioBytes");
71 if (audioFormat == null)
72 throw new NullPointerException("audioFormat");
73 if (localFilename == null || localFilename.length() == 0)
74 throw new NullPointerException("localFilename");
75
76 assert(audioBytes.length > 0);
77
78 // Check format
79 if (!SampledAudioManager.getInstance().isFormatSupportedForPlayback(audioFormat))
80 throw new IllegalArgumentException("Bad audioFormat");
81
82 this.audioFormat = audioFormat;
83 this.audioBytes = audioBytes;
84 this.localFilename = localFilename;
85
86 }
87
88
89 public void detectBeats(int audioBufferSize, int audioBufferOverlap) throws UnsupportedAudioFileException {
90 AudioDispatcher dispatcher = AudioDispatcherFactory.fromByteArray(audioBytes, audioFormat,
91 audioBufferSize, audioBufferOverlap);
92
93 ComplexOnsetDetector detector = new ComplexOnsetDetector(audioBufferSize);
94 BeatRootOnsetEventHandler handler = new BeatRootOnsetEventHandler();
95 detector.setHandler(handler);
96
97 dispatcher.addAudioProcessor(detector);
98 dispatcher.run();
99
100 beats = new ArrayList<Double>();
101 handler.trackBeats(this);
102
103 }
104
105 @Override
106 public void handleOnset(double time, double salience) {
107 beats.add(time);
108 }
109
110 public List<Double> getBeats() {
111 return beats;
112 }
113
114 public void clearBeats() {
115 beats = null;;
116 }
117
118
119
120
121 /**
122 * @return True if audio bytes have been changed in some way since creation / last reset.
123 */
124 public boolean isAudioModified() {
125 return isAudioModified;
126 }
127
128 /**
129 * Sets modified flag.
130 */
131 public void setAudioModifiedFlag(boolean isModified) {
132 isAudioModified = isModified;
133 }
134
135 /**
136 * Same as fireSubjectChanged but also sets modified flag
137 */
138 private void fireAudioBytesChanged(final SubjectChangedEvent event) {
139 isAudioModified = true;
140 fireSubjectChanged(event);
141 }
142
143 /**
144 * @return The audio format of the sampled bytes
145 */
146 public AudioFormat getFormat() {
147 return audioFormat;
148 }
149
150 /**
151 * @return The amount of frames contained in this audio track
152 */
153 public int getFrameCount() {
154 return audioBytes.length / audioFormat.getFrameSize();
155 }
156
157
158 /**
159 * @return The start of selection in frames.
160 */
161 public int getSelectionStart() {
162 return selectionStart;
163 }
164
165 /**
166 * @return The length of selection in frames. less or equal to one if selection length is one frame.
167 * Otherwise selection is ranged.
168 */
169 public int getSelectionLength() {
170 return selectionLength;
171 }
172
173 /**
174 * Sets the selection. Clamped
175 *
176 * @param start
177 * Must be larger or equal to zero. In frames.
178 * @param length
179 * In frames. If less or equal to one, then the selection length is one frame.
180 * Otherwise selection is ranged.
181 */
182 public void setSelection(int start, int length) {
183
184 if (start < 0) start = 0;
185
186 if ((start + length) > getFrameCount())
187 length = getFrameCount() - start;
188
189 selectionStart = start;
190 selectionLength = length;
191
192 fireSubjectChanged(new SubjectChangedEvent(
193 ApolloSubjectChangedEvent.SELECTION_CHANGED));
194 }
195
196 /**
197 * @return The audio bytes of the selected frames
198 */
199 public byte[] getSelectedFramesCopy() {
200
201 int len = (selectionLength <= 0) ? 1 : selectionLength;
202 len *= audioFormat.getFrameSize();
203
204 byte[] selectedBytes = new byte[len];
205
206 System.arraycopy(
207 audioBytes,
208 selectionStart * audioFormat.getFrameSize(),
209 selectedBytes,
210 0,
211 len);
212
213 return selectedBytes;
214 }
215
216 /**
217 * Removes the selected frames. If no <i>range</i> is selected then it will return immediatly.
218 * raises a {@link ApolloSubjectChangedEvent#AUDIO_REMOVED} event if removed frames.
219 *
220 * Resets the selection length to nothing once removed the frames (selection start
221 * remains the same). Thus raises a selection changed event.
222 *
223 * Does not allow removeing of all bytes ... must have at least frame left.
224 *
225 * Can be playing in a playing state - as nothing "should" break.
226 */
227 public void removeSelectedBytes() {
228
229 if(selectionLength <= 1) throw new IllegalStateException("No range selected to remove");
230
231 int len = selectionLength * audioFormat.getFrameSize();
232 int start = selectionStart * audioFormat.getFrameSize();
233
234 if ((audioBytes.length - len) == 0) return;
235
236 byte[] newAudioBytes = new byte[audioBytes.length - len];
237
238 // Copy first chunk of bytes before selection
239 System.arraycopy(
240 audioBytes, 0,
241 newAudioBytes, 0,
242 start);
243
244 // Copy remaining bytes after selection
245 System.arraycopy(
246 audioBytes, start + len,
247 newAudioBytes, start,
248 audioBytes.length - (start + len));
249
250 audioBytes = newAudioBytes;
251
252 setSelection(selectionStart, 0); // raises the event.
253
254 fireAudioBytesChanged(new SubjectChangedEvent(
255 ApolloSubjectChangedEvent.AUDIO_REMOVED));
256 }
257
258 /**
259 * Inserts bytes <b>AFTER</b> a given frame position.
260 * <b>TAKE NOTE:</b> Not zero-indexed. One-indexed.
261 *
262 * @param bytesToAdd
263 * The bytes to add. Must not be null and be length larger than zero.
264 *
265 * @param format
266 * The format of the audio bytes to add. Must not be null.
267 *
268 * @param framePosition
269 * Zero is the lower bound case, where the bytes are added at the very beggining.
270 * The upper bound case is the total frame-count of this track model, where the bytes
271 * are added to the very end of the track.
272 *
273 * @throws IOException
274 * If an error occured while converting
275 *
276 * @throws IllegalArgumentException
277 * If the conversion is not supported.
278 *
279 */
280 public void insertBytes(byte[] bytesToAdd, AudioFormat format, int framePosition)
281 throws IOException {
282
283 assert(format != null);
284 assert(bytesToAdd != null);
285 assert(bytesToAdd.length > 0);
286 assert(framePosition >= 0);
287 assert(framePosition <= getFrameCount());
288 assert(SampledAudioManager.getInstance().isFormatSupportedForPlayback(format));
289
290 // Convert format - if needs to
291 byte[] normalizedBytes = AudioIO.convertAudioBytes(bytesToAdd, format, audioFormat);
292 assert(normalizedBytes != null);
293
294 // Insert bytes at frame position
295 int insertBytePosition = framePosition * audioFormat.getFrameSize();
296 byte[] newAudioBytes = new byte[audioBytes.length + normalizedBytes.length];
297
298 // Add preceeding audio bytes from original track
299 if (framePosition > 0)
300 System.arraycopy(
301 audioBytes, 0,
302 newAudioBytes, 0,
303 insertBytePosition);
304
305 // Add the inserted audio bytes
306 System.arraycopy(
307 normalizedBytes, 0,
308 newAudioBytes, insertBytePosition,
309 normalizedBytes.length);
310
311 // Append the proceeding audio bytes of the original track
312 if (framePosition < getFrameCount())
313 System.arraycopy(
314 audioBytes, insertBytePosition,
315 newAudioBytes, insertBytePosition + normalizedBytes.length,
316 audioBytes.length - insertBytePosition);
317
318 // Assign the new bytes
319 audioBytes = newAudioBytes;
320
321 // Notify observers
322 fireAudioBytesChanged(new SubjectChangedEvent(
323 ApolloSubjectChangedEvent.AUDIO_INSERTED));
324
325 }
326
327 /**
328 * @see SampledTrackModel#getAllAudioBytesCopy()
329 *
330 * @return The <b>actual</b>(same reference) audio bytes for this model.
331 */
332 public byte[] getAllAudioBytes() {
333 return audioBytes;
334 }
335
336 /**
337 * @see SampledTrackModel#getAllAudioBytes()
338 *
339 * @return ALl the audio bytes - copied.
340 */
341 public byte[] getAllAudioBytesCopy() {
342 byte[] copy = new byte[audioBytes.length];
343 System.arraycopy(audioBytes, 0, copy, 0, audioBytes.length);
344 return copy;
345 }
346
347 /**
348 * @return
349 * The file path associtaed to this model.
350 * Null if none is set.
351 */
352 public String getFilepath() {
353 return currentFilepath;
354 }
355
356 /**
357 * @param filepath
358 * The filename to associate with this model.
359 * Can be null.
360 */
361 public void setFilepath(String filepath) {
362 this.currentFilepath = filepath;
363 }
364
365 /**
366 * @return
367 * The name associated to this tack. Can be null.
368 *
369 * @see #setName(String)
370 *
371 */
372 public String getName() {
373 return name;
374 }
375
376 /**
377 * Sets the name for this track. Might be useful for identifying a track in
378 * a human readable format.
379 *
380 * Note that the name is not used for anything inside this package - it is
381 * for the convience of the user only.
382 *
383 * creates a {@link ApolloSubjectChangedEvent#NAME_CHANGED} event
384 *
385 * @param name
386 * The name associated to this tack. Can be null.
387 */
388 public void setName(String name) {
389 if (name != this.name) {
390 this.name = name;
391 fireSubjectChanged(new SubjectChangedEvent(
392 ApolloSubjectChangedEvent.NAME_CHANGED, null));
393 }
394 }
395
396 /**
397 * @return
398 * The immutable local filename associated with this track model.
399 */
400 public String getLocalFilename() {
401 return localFilename;
402 }
403
404}
Note: See TracBrowser for help on using the repository browser.