1 | package org.apollo.audio;
|
---|
2 |
|
---|
3 | import java.io.IOException;
|
---|
4 | import java.io.PipedInputStream;
|
---|
5 | import java.io.PipedOutputStream;
|
---|
6 |
|
---|
7 | import javax.sound.sampled.AudioFormat;
|
---|
8 | import javax.sound.sampled.DataLine;
|
---|
9 | import javax.sound.sampled.LineUnavailableException;
|
---|
10 | import javax.sound.sampled.TargetDataLine;
|
---|
11 |
|
---|
12 | import org.apollo.mvc.AbstractSubject;
|
---|
13 | import org.apollo.mvc.SubjectChangedEvent;
|
---|
14 |
|
---|
15 | /**
|
---|
16 | * Provides sampled recording services.
|
---|
17 | *
|
---|
18 | * @author Brook Novak
|
---|
19 | *
|
---|
20 | */
|
---|
21 | public class RecordManager extends AbstractSubject
|
---|
22 | {
|
---|
23 | private AudioCaptureThread captureThread = null;
|
---|
24 |
|
---|
25 | private static RecordManager instance = new RecordManager(); // single design pattern
|
---|
26 |
|
---|
27 | /**
|
---|
28 | * @return The singleton instance of the SampledAudioManager
|
---|
29 | */
|
---|
30 | public static RecordManager getInstance() { // single design pattern
|
---|
31 | return instance;
|
---|
32 | }
|
---|
33 |
|
---|
34 | private RecordManager() { // singleton design pattern
|
---|
35 | }
|
---|
36 |
|
---|
37 | /**
|
---|
38 | * Stops all captures... kills thread. Halts until killed.
|
---|
39 | */
|
---|
40 | public void releaseResources() {
|
---|
41 |
|
---|
42 | // Quickly stop playback thread
|
---|
43 | if (isCapturing())
|
---|
44 | stopCapturing(); // halts
|
---|
45 | }
|
---|
46 |
|
---|
47 | /**
|
---|
48 | * @return True if audio is currently being captured.
|
---|
49 | */
|
---|
50 | public boolean isCapturing() {
|
---|
51 | return (captureThread != null && captureThread.isAlive());
|
---|
52 | }
|
---|
53 |
|
---|
54 | /**
|
---|
55 | * Stops capturing audio if any is being captured. Waits until capture proccess
|
---|
56 | * stops.
|
---|
57 | *
|
---|
58 | * A CAPTURE_STOPPED event will be raised by invoking this method if there was
|
---|
59 | * audio being captured prior to invoking this method.
|
---|
60 | */
|
---|
61 | public void stopCapturing() {
|
---|
62 | // Tell the capture thread to stop
|
---|
63 | if (captureThread != null) {
|
---|
64 | captureThread.stopCapturing();
|
---|
65 | captureThread = null;
|
---|
66 | }
|
---|
67 | }
|
---|
68 |
|
---|
69 | /**
|
---|
70 | * Begins capturing audio from the current input mixture.
|
---|
71 | *
|
---|
72 | * If audio is currently being captured, that capture process is stopped
|
---|
73 | * (hence a CAPTURE_STOPPED event will be raised).
|
---|
74 | *
|
---|
75 | * A CAPTURE_STARTED event will be raised by invoking this method. Once the record thread has started
|
---|
76 | *
|
---|
77 | * @param state
|
---|
78 | * An object for optional state info - this is included in the audio event.
|
---|
79 | * The intention being that observers can ID whether the start/stop events are
|
---|
80 | * relevelent to them or not...
|
---|
81 | *
|
---|
82 | * @return
|
---|
83 | * A AudioCapturePipe which the recorded bytes will be written to.
|
---|
84 | *
|
---|
85 | * @throws LineUnavailableException
|
---|
86 | * If a line from the current input mixer is not available.
|
---|
87 | * (could be used by another proccess/application)
|
---|
88 | *
|
---|
89 | * @throws IOException
|
---|
90 | * If failed to create a pipe.
|
---|
91 | */
|
---|
92 | public synchronized AudioCapturePipe captureAudio(Object state)
|
---|
93 | throws LineUnavailableException, IOException {
|
---|
94 |
|
---|
95 | // Check to see if input mixer is available
|
---|
96 | if (SampledAudioManager.getInstance().getCurrentInputMixure() == null) {
|
---|
97 | throw new LineUnavailableException("No input mixer avialable");
|
---|
98 | }
|
---|
99 |
|
---|
100 | if (SampledAudioManager.getInstance().getDefaultCaptureFormat() == null) {
|
---|
101 | throw new LineUnavailableException("The current input mixer does not support audio capture in a valid format");
|
---|
102 | }
|
---|
103 |
|
---|
104 |
|
---|
105 | // Stop capturing any audio
|
---|
106 | stopCapturing();
|
---|
107 |
|
---|
108 | // Create pipe
|
---|
109 | PipedOutputStream pout = new PipedOutputStream();
|
---|
110 | PipedInputStream pin = new PipedInputStream(pout);
|
---|
111 |
|
---|
112 | // Begin capturing
|
---|
113 | assert (captureThread == null || !captureThread.isAlive());
|
---|
114 | captureThread = new AudioCaptureThread(pout, state);
|
---|
115 | captureThread.start();
|
---|
116 |
|
---|
117 | return new AudioCapturePipe(pin, captureThread.format, captureThread.bufferSize);
|
---|
118 | }
|
---|
119 |
|
---|
120 |
|
---|
121 | /**
|
---|
122 | * This inner class is used for asynchronously read data from a target data line
|
---|
123 | * to a stream; i.e. audio capture.
|
---|
124 | *
|
---|
125 | * Once started, the audio from a TargetDataLine which is linked ot the input mixer
|
---|
126 | * until either stopCapturing is invoked or the output stream is closed.
|
---|
127 | *
|
---|
128 | * Fires Capture start/stop events once the thread starts / finishes respectively
|
---|
129 | *
|
---|
130 | * @author Brook Novak
|
---|
131 | */
|
---|
132 | class AudioCaptureThread extends Thread {
|
---|
133 |
|
---|
134 | private boolean isCancelled = false;
|
---|
135 |
|
---|
136 | private TargetDataLine tdl;
|
---|
137 |
|
---|
138 | private Object state;
|
---|
139 |
|
---|
140 | private PipedOutputStream pout;
|
---|
141 |
|
---|
142 | private AudioFormat format;
|
---|
143 |
|
---|
144 | private int bufferSize;
|
---|
145 |
|
---|
146 | /**
|
---|
147 | * Constructor.
|
---|
148 | * The input mixer must not be null.
|
---|
149 | *
|
---|
150 | * @param state
|
---|
151 | * An object for optional state info - this is included in the audio event.
|
---|
152 | * The intention being that observers can ID whether the start/stop events are
|
---|
153 | * relevelent to them or not...
|
---|
154 | *
|
---|
155 | * @param pout
|
---|
156 | * The output stream to write to.
|
---|
157 | *
|
---|
158 | * @throws LineUnavailableException
|
---|
159 | * If a line from the current mixer is not available.
|
---|
160 | * (could be used by another proccess/application)
|
---|
161 | *
|
---|
162 | */
|
---|
163 | AudioCaptureThread(PipedOutputStream pout, Object state)
|
---|
164 | throws LineUnavailableException {
|
---|
165 |
|
---|
166 | assert(pout != null);
|
---|
167 | assert(SampledAudioManager.getInstance().getCurrentInputMixure() != null);
|
---|
168 | assert(SampledAudioManager.getInstance().getDefaultCaptureFormat() != null);
|
---|
169 |
|
---|
170 | this.state = state;
|
---|
171 | this.pout = pout;
|
---|
172 |
|
---|
173 | this.format = SampledAudioManager.getInstance().getDefaultCaptureFormat();
|
---|
174 |
|
---|
175 | // Select a target data line
|
---|
176 | DataLine.Info dlInfo = new DataLine.Info(TargetDataLine.class, format);
|
---|
177 |
|
---|
178 | // The current input mixer should always support the defaultCaptureFormat
|
---|
179 | assert(SampledAudioManager.getInstance().getCurrentInputMixure().isLineSupported(dlInfo));
|
---|
180 |
|
---|
181 | tdl = (TargetDataLine) SampledAudioManager.getInstance().getCurrentInputMixure().getLine(dlInfo);
|
---|
182 |
|
---|
183 | tdl.open(format); // Throws LineUnavailableException
|
---|
184 |
|
---|
185 | // Ensure a multiple of frame size
|
---|
186 | bufferSize = tdl.getBufferSize();
|
---|
187 | bufferSize -= (bufferSize % format.getFrameSize());
|
---|
188 | if (bufferSize <= 0) bufferSize = format.getFrameSize();
|
---|
189 | }
|
---|
190 |
|
---|
191 | /**
|
---|
192 | * Stops capturing the stream. Waits until thread stopped
|
---|
193 | * Can return without the thread actually stopping (yet) if the calling thread
|
---|
194 | * is interupted during the wait.
|
---|
195 | */
|
---|
196 | public void stopCapturing() {
|
---|
197 | isCancelled = true;
|
---|
198 | if (isAlive()) {
|
---|
199 | tdl.stop(); // blocks (wait time can vary depending on formats..some may take awhile!)
|
---|
200 | tdl.drain();
|
---|
201 | tdl.close();
|
---|
202 | try {
|
---|
203 | join();
|
---|
204 | } catch (InterruptedException e) { /* Consume */
|
---|
205 | }
|
---|
206 | }
|
---|
207 | }
|
---|
208 |
|
---|
209 | @Override
|
---|
210 | public void run() {
|
---|
211 |
|
---|
212 | // Notify observers
|
---|
213 | fireSubjectChangedLaterOnSwingThread(new SubjectChangedEvent(
|
---|
214 | ApolloSubjectChangedEvent.CAPTURE_STARTED, state));
|
---|
215 |
|
---|
216 | byte buffer[] = new byte[bufferSize];
|
---|
217 |
|
---|
218 | try {
|
---|
219 | // Start capturing
|
---|
220 | tdl.start();
|
---|
221 |
|
---|
222 | // Dump bytes into pipe
|
---|
223 | while (!isCancelled) {
|
---|
224 |
|
---|
225 | int count = tdl.read(buffer, 0, buffer.length);
|
---|
226 |
|
---|
227 | if (count > 0) {
|
---|
228 | pout.write(buffer, 0, count);
|
---|
229 | }
|
---|
230 | }
|
---|
231 |
|
---|
232 | } catch (IOException e) { // Failed to write to pipe
|
---|
233 | e.printStackTrace();
|
---|
234 |
|
---|
235 | } finally {
|
---|
236 |
|
---|
237 | // If thread execution stopped in a way other than the TDL closing ... ensure that
|
---|
238 | // it is actually closed.
|
---|
239 | if (tdl.isOpen()) {
|
---|
240 | tdl.close();
|
---|
241 | }
|
---|
242 |
|
---|
243 | // Close off pipe - will release reader from blocking and notify them that finished
|
---|
244 | try {
|
---|
245 | pout.close();
|
---|
246 | } catch (IOException e) { /* Consume */ }
|
---|
247 |
|
---|
248 | if (Metronome.getInstance().isEnabled() &&
|
---|
249 | Metronome.getInstance().isPlaying()) {
|
---|
250 |
|
---|
251 | }
|
---|
252 |
|
---|
253 | // Notify observers
|
---|
254 | fireSubjectChangedLaterOnSwingThread(new SubjectChangedEvent(
|
---|
255 | ApolloSubjectChangedEvent.CAPTURE_STOPPED,
|
---|
256 | state));
|
---|
257 | }
|
---|
258 |
|
---|
259 | }
|
---|
260 |
|
---|
261 | }
|
---|
262 |
|
---|
263 | }
|
---|