f@0
|
1 /*
|
f@0
|
2 Cross-Modal DAW Prototype - Prototype of a simple Cross-Modal Digital Audio Workstation.
|
f@0
|
3
|
f@0
|
4 Copyright (C) 2015 Queen Mary University of London (http://depic.eecs.qmul.ac.uk/)
|
f@0
|
5
|
f@0
|
6 This program is free software: you can redistribute it and/or modify
|
f@0
|
7 it under the terms of the GNU General Public License as published by
|
f@0
|
8 the Free Software Foundation, either version 3 of the License, or
|
f@0
|
9 (at your option) any later version.
|
f@0
|
10
|
f@0
|
11 This program is distributed in the hope that it will be useful,
|
f@0
|
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
|
f@0
|
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
f@0
|
14 GNU General Public License for more details.
|
f@0
|
15
|
f@0
|
16 You should have received a copy of the GNU General Public License
|
f@0
|
17 along with this program. If not, see <http://www.gnu.org/licenses/>.
|
f@0
|
18 */
|
f@0
|
19
|
f@0
|
20 package uk.ac.qmul.eecs.depic.daw;
|
f@0
|
21
|
f@0
|
22 import java.io.BufferedInputStream;
|
f@0
|
23 import java.io.File;
|
f@0
|
24 import java.nio.ByteBuffer;
|
f@0
|
25 import java.nio.ByteOrder;
|
f@0
|
26 import java.util.ArrayList;
|
f@0
|
27 import java.util.List;
|
f@0
|
28
|
f@0
|
29 import javax.sound.sampled.AudioFormat;
|
f@0
|
30 import javax.sound.sampled.AudioInputStream;
|
f@0
|
31 import javax.sound.sampled.AudioSystem;
|
f@0
|
32 import javax.sound.sampled.UnsupportedAudioFileException;
|
f@0
|
33 import javax.swing.SwingWorker;
|
f@0
|
34
|
f@0
|
35 import uk.ac.qmul.eecs.depic.daw.AudioLoader.ReturnObject;
|
f@0
|
36
|
f@1
|
37 /**
|
f@1
|
38 *
|
f@1
|
39 * A swing worker that loads an audio file in a separate thread. It returns a RetunObject when the loading is complete.
|
f@1
|
40 *
|
f@1
|
41 */
|
f@0
|
42 public class AudioLoader extends SwingWorker<ReturnObject,Void>{
|
f@0
|
43 public static final int FILE_LOAD_TOTAL_PROGRESS = 100;
|
f@0
|
44 /**
|
f@0
|
45 * The default conversion format used. Also the conversion format returned by {@code getConversionFormat()}
|
f@0
|
46 */
|
f@0
|
47 public static final AudioFormat DEFAULT_CONVERSION_FORMAT = new AudioFormat(
|
f@0
|
48 8000.0f, // sample rate
|
f@0
|
49 16, // bit per sample
|
f@0
|
50 1, // mono
|
f@0
|
51 true, // signed
|
f@0
|
52 false // little endian (default .wav files)
|
f@0
|
53 );
|
f@0
|
54
|
f@0
|
55 private File audioFile;
|
f@0
|
56 private int minChunkSize;
|
f@0
|
57 private int maxScaleFactor;
|
f@0
|
58
|
f@0
|
59 public AudioLoader(File audioFile, int minChunkSize, int maxScaleFactor){
|
f@0
|
60 this.audioFile = audioFile;
|
f@0
|
61 this.minChunkSize = minChunkSize;
|
f@0
|
62 this.maxScaleFactor = maxScaleFactor;
|
f@0
|
63 }
|
f@0
|
64
|
f@0
|
65 /**
|
f@0
|
66 * Reads the audio files and build all the min and max for all the shunks of frames
|
f@0
|
67 */
|
f@0
|
68 @Override
|
f@0
|
69 protected ReturnObject doInBackground() throws Exception {
|
f@0
|
70 /* get all the info about the file format, needed later for the estimate of the converted file length */
|
f@0
|
71 AudioInputStream originalFile = AudioSystem.getAudioInputStream(audioFile);
|
f@0
|
72
|
f@0
|
73 AudioFormat originalAudioFormat = originalFile.getFormat();
|
f@0
|
74 long originalNumTotalFrames = originalFile.getFrameLength();
|
f@0
|
75 float originalFrameSize = originalAudioFormat.getFrameSize();
|
f@0
|
76 float originalFrameRate = originalAudioFormat.getFrameRate();
|
f@0
|
77 float originalNumChannels = originalAudioFormat.getChannels();
|
f@0
|
78
|
f@0
|
79 if(originalNumTotalFrames == 0)
|
f@0
|
80 throw new UnsupportedAudioFileException("File Empty");
|
f@0
|
81 /* convert the audio format to the one suitable for parsing chunks and get min and max from them (see getConversionFormat()) */
|
f@0
|
82 AudioFormat conversionFormat = getConversionFormat();
|
f@0
|
83 if(!AudioSystem.isConversionSupported(conversionFormat,originalAudioFormat)){
|
f@0
|
84 throw new UnsupportedAudioFileException("Cannot convert file to the following format: "+conversionFormat);
|
f@0
|
85 }
|
f@0
|
86
|
f@0
|
87 AudioInputStream convertedFile = AudioSystem.getAudioInputStream(conversionFormat, originalFile);
|
f@0
|
88 /* start parsing the file and building the chunks' minimums and maximums */
|
f@0
|
89 /* all the variable from here on, unless they begin with "originalFile" refer to the converted audio stream */
|
f@0
|
90
|
f@0
|
91 byte[] audioBytes = new byte[minChunkSize * conversionFormat.getFrameSize()];
|
f@0
|
92 /* prepare the ByteBuffer, wrapping the byte array, that will be used to read Short values */
|
f@0
|
93 ByteBuffer byteBuffer = ByteBuffer.wrap(audioBytes);
|
f@0
|
94 /* set the endiannes according to the audio format */
|
f@0
|
95 byteBuffer.order(conversionFormat.isBigEndian() ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN);
|
f@0
|
96
|
f@0
|
97 /* make an estimation of the frames in the converted file, based on the original file. This is necessary because *
|
f@0
|
98 * convertedFile is a stream pointing to original file and not a file itself. Therefore getFrameLength() returns *
|
f@0
|
99 * -1 as t doesn't have any knowledge of the length of the underlying file. So make an estimate of the length *
|
f@0
|
100 * of the file after conversion in order to allocate enough space when the ArrayList in newFlechunks is allocated*
|
f@0
|
101 * and for the progress of file load. */
|
f@0
|
102 float convertedFrameSize = conversionFormat.getFrameSize();
|
f@0
|
103 float convertedFrameRate = conversionFormat.getFrameRate();
|
f@0
|
104 float convertedNumChannels = conversionFormat.getChannels();
|
f@0
|
105
|
f@0
|
106 long convertedNumTotalFrames = (long) (
|
f@0
|
107 originalNumTotalFrames *
|
f@0
|
108 (convertedFrameSize/originalFrameSize) *
|
f@0
|
109 (convertedFrameRate/originalFrameRate) *
|
f@0
|
110 (convertedNumChannels/originalNumChannels));
|
f@0
|
111 long convertedNumTotalBytes = convertedNumTotalFrames * conversionFormat.getFrameSize();
|
f@0
|
112
|
f@0
|
113 /* creates the first list of chunks with smallest size. Array size = audio frames/num frames of minimum chunk */
|
f@0
|
114 WavePeaks newFileChunks = new WavePeaks(maxScaleFactor);
|
f@0
|
115 /* first List s for scale factor = 1, that is the finest zoom scale */
|
f@0
|
116 newFileChunks.add(1, new ArrayList<Chunk>((int)(convertedNumTotalFrames/minChunkSize) +1));
|
f@0
|
117 int numBytesRead = 0;
|
f@0
|
118 float totalBytesRead = 0f;
|
f@0
|
119 try(BufferedInputStream chunkBufferedAudio = new BufferedInputStream(convertedFile,audioBytes.length)){
|
f@0
|
120 while((numBytesRead = chunkBufferedAudio.read(audioBytes)) != -1){
|
f@0
|
121 if(isCancelled())
|
f@0
|
122 return null;
|
f@0
|
123 totalBytesRead += numBytesRead;
|
f@0
|
124 /* normalize the progress value to total load */
|
f@0
|
125 int progress = (int) ((totalBytesRead/convertedNumTotalBytes)*FILE_LOAD_TOTAL_PROGRESS);
|
f@0
|
126 if(progress < FILE_LOAD_TOTAL_PROGRESS)
|
f@0
|
127 setProgress(progress);
|
f@0
|
128 /* Now read the byte buffer, backed by audioByte, and find min and max. The audio format *
|
f@0
|
129 * has been converted to signed 16 bit frames, so it can be read in a Short value */
|
f@0
|
130 Short currentMax = Short.MIN_VALUE;
|
f@0
|
131 Short currentMin = Short.MAX_VALUE;
|
f@0
|
132
|
f@0
|
133 /* find maximum and minimum values in this chunk */
|
f@0
|
134 byteBuffer.clear();
|
f@0
|
135 byteBuffer.limit(numBytesRead);
|
f@0
|
136 while(byteBuffer.hasRemaining()){
|
f@0
|
137 Short frame = byteBuffer.getShort();
|
f@0
|
138 if(frame > currentMax)
|
f@0
|
139 currentMax = frame;
|
f@0
|
140 if(frame < currentMin)
|
f@0
|
141 currentMin = frame;
|
f@0
|
142 }
|
f@0
|
143 newFileChunks.get(1).add(new Chunk(currentMin, currentMax));
|
f@0
|
144 }
|
f@0
|
145 }
|
f@0
|
146
|
f@0
|
147 for(int scaleFactor = 2; scaleFactor <= maxScaleFactor; scaleFactor++){
|
f@0
|
148 List<Chunk> previousList = newFileChunks.get(scaleFactor-1);
|
f@0
|
149 List<Chunk> newList = new ArrayList<>(previousList.size()/2+1);
|
f@0
|
150
|
f@0
|
151 for(int i=0; i<previousList.size();i += 2){
|
f@0
|
152 /* check if we're at the last array item, which happens when the size is odd *
|
f@0
|
153 * In this case we don't merge two items but just take the last item as a new one */
|
f@0
|
154 if(i == previousList.size()-1){
|
f@0
|
155 newList.add(previousList.get(i));
|
f@0
|
156 break; // end of the array anyway
|
f@0
|
157 }
|
f@0
|
158 newList.add(new Chunk(previousList.get(i),previousList.get(i+1)));
|
f@0
|
159 }
|
f@0
|
160 newFileChunks.add(scaleFactor, newList);
|
f@0
|
161 }
|
f@0
|
162
|
f@0
|
163 /* open the Sample for playback */
|
f@1
|
164 Sample sample = Daw.getSoundEngineFactory().createSample(audioFile.getAbsolutePath());
|
f@1
|
165
|
f@1
|
166
|
f@0
|
167 /* return sample and chunks to the event dispatching thread */
|
f@0
|
168 return new ReturnObject(newFileChunks,sample,originalAudioFormat,conversionFormat);
|
f@0
|
169 }
|
f@0
|
170
|
f@0
|
171 protected AudioFormat getConversionFormat(){
|
f@0
|
172 return DEFAULT_CONVERSION_FORMAT;
|
f@0
|
173 }
|
f@0
|
174
|
f@1
|
175 /**
|
f@1
|
176 *
|
f@1
|
177 * An object returned by the AudioLoader. It contains meta data about the sound sample such as wave peaks and format
|
f@1
|
178 * as well as the Sample object representing the loaded sample.
|
f@1
|
179 *
|
f@1
|
180 */
|
f@0
|
181 public static class ReturnObject {
|
f@1
|
182 public ReturnObject(WavePeaks peaks, Sample s,
|
f@0
|
183 AudioFormat originalFormat, AudioFormat conversionFormat) {
|
f@0
|
184 super();
|
f@0
|
185 this.peaks = peaks;
|
f@0
|
186 this.sample = s;
|
f@0
|
187 this.originalFormat = originalFormat;
|
f@0
|
188 this.conversionFormat = conversionFormat;
|
f@0
|
189 }
|
f@0
|
190
|
f@0
|
191 public WavePeaks peaks;
|
f@1
|
192 public Sample sample;
|
f@0
|
193 public AudioFormat originalFormat;
|
f@0
|
194 public AudioFormat conversionFormat;
|
f@0
|
195 }
|
f@0
|
196 }
|