source: trunk/src_apollo/org/apollo/audio/SampledTrackModel.java@ 315

Last change on this file since 315 was 315, checked in by bjn8, 16 years ago

Apollo spin-off added

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