001 /*
002 Copyright (c) 1996-2011, Damon Hart-Davis
003 All rights reserved.
004
005 Redistribution and use in source and binary forms, with or without
006 modification, are permitted provided that the following conditions are
007 met:
008
009 * Redistributions of source code must retain the above copyright
010 notice, this list of conditions and the following disclaimer.
011
012 * Redistributions in binary form must reproduce the above copyright
013 notice, this list of conditions and the following disclaimer in the
014 documentation and/or other materials provided with the
015 distribution.
016
017 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
018 IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
019 TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
020 PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
021 OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
022 SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
023 LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
024 DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
025 THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
026 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
027 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
028 */
029
030 package org.hd.d.pg2k.svrCore.MIME;
031
032 import java.io.IOException;
033 import java.io.InputStream;
034 import java.util.Map;
035
036 import javax.sound.sampled.AudioFileFormat;
037 import javax.sound.sampled.AudioFormat;
038 import javax.sound.sampled.AudioSystem;
039 import javax.sound.sampled.UnsupportedAudioFileException;
040 import javax.xml.parsers.DocumentBuilder;
041 import javax.xml.parsers.DocumentBuilderFactory;
042 import javax.xml.parsers.ParserConfigurationException;
043
044 import org.hd.d.pg2k.svrCore.Name.ExhibitFull;
045 import org.hd.d.pg2k.svrCore.TextUtils;
046 import org.w3c.dom.Document;
047 import org.w3c.dom.Element;
048 import org.w3c.dom.Node;
049
050 /**Base class with useful default behaviour for (sampled) sound exhibit media-handler classes.
051 * By default this uses javax.sound.* routines to extract sound and metadata from
052 * an exhibit stream.
053 * <p>
054 * Various routines can be overridden in deriving classes to change this behaviour.
055 */
056 public abstract class AbstractSampledSoundHandler extends AbstractHandler
057 {
058 /**Get format of audio data; null if none or format not usable/parseable.
059 * This may require the stream to be seekable, at least with mark() and reset().
060 */
061 protected AudioFileFormat getAudioFileFormat(final InputStream is)
062 {
063 try { return(AudioSystem.getAudioFileFormat(is)); }
064
065 // Dump any (expected) errors to the log/console and return null.
066 catch(final UnsupportedAudioFileException e)
067 {
068 // e.printStackTrace();
069 }
070 catch(final IOException e)
071 {
072 // e.printStackTrace();
073 }
074
075 return(null); // Could not deduce file format.
076 }
077
078 /**Gets all available exhibit metadata as a single XML DOM tree; null if none.
079 * We do not (yet) have a schema for this and may never do so,
080 * since its structure and content will depend on various external sources
081 * and parts of (for example) the ImageIO and JAI subsystems.
082 * <p>
083 * This implementation synthesises a metadata tree from the AudioFileFormat data,
084 * if any.
085 *
086 * @param is input stream; never null
087 * @return top-level node "metadata" with captured metadata beneath, else null
088 */
089 @Override
090 public Node getMetadata(final InputStream is, final ExhibitFull exhibitName)
091 {
092 final AudioFileFormat aff = getAudioFileFormat(is);
093 if(aff == null) { return(null); }
094
095 final AudioFormat af = aff.getFormat();
096 if(af == null) { return(null); }
097
098 // Result: will be set non-null if we encounter any usable data.
099 Node result = null;
100
101 try
102 {
103 final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
104 final DocumentBuilder db = dbf.newDocumentBuilder();
105 final Document doc = db.newDocument();
106
107 // Create a provisional root node,
108 // but only assign it to the result (ie to get a non-null result)
109 // when we know that there is some real data in there.
110 final Element root = doc.createElement(TAG_NAME_METADATA_TOP);
111 doc.appendChild(root);
112
113 // The "stream" node contains data about the file format.
114 final Element stream = doc.createElement("stream");
115 root.appendChild(stream);
116
117 final int nFrames = aff.getFrameLength();
118 if(nFrames != AudioSystem.NOT_SPECIFIED)
119 {
120 final Element framesNode = doc.createElement("frames");
121 framesNode.setAttribute("value", String.valueOf(nFrames));
122 stream.appendChild(framesNode);
123 result = root; // Got some real data...
124 }
125
126 final Map<String,Object> sPropMap = aff.properties();
127 if((sPropMap != null) && (!sPropMap.isEmpty()))
128 {
129 final Element propsNode = doc.createElement("properties");
130 // Insert all the properties.
131 for(final String key : sPropMap.keySet())
132 {
133 final Element propNode = doc.createElement("property");
134 propNode.setAttribute("name", TextUtils.escapeHTMLMetaChars(key));
135 propNode.setAttribute("value", TextUtils.escapeHTMLMetaChars(String.valueOf(sPropMap.get(key))));
136 propsNode.appendChild(propNode);
137 }
138 stream.appendChild(propsNode);
139 result = root; // Got some real data...
140 }
141
142 // The "audio" node contains data about the data encoding format such as rate.
143 final Element audio = doc.createElement("audio");
144 root.appendChild(audio);
145
146 final AudioFormat.Encoding encoding = af.getEncoding();
147 if(encoding != null)
148 {
149 final Element encodingNode = doc.createElement("encoding");
150 encodingNode.setAttribute("value", TextUtils.escapeHTMLMetaChars(encoding.toString()));
151 audio.appendChild(encodingNode);
152 result = root; // Got some real data...
153 }
154
155 final float sampleRate = af.getSampleRate();
156 if(sampleRate != AudioSystem.NOT_SPECIFIED)
157 {
158 final Element sampleRateNode = doc.createElement("sampleRate");
159 sampleRateNode.setAttribute("value", TextUtils.escapeHTMLMetaChars(String.valueOf(sampleRate)));
160 sampleRateNode.setAttribute("unit", "Hz");
161 audio.appendChild(sampleRateNode);
162 result = root; // Got some real data...
163 }
164
165 final int sampleSizeInBits = af.getSampleSizeInBits();
166 if(sampleSizeInBits != AudioSystem.NOT_SPECIFIED)
167 {
168 final Element sampleSizeNode = doc.createElement("sampleSize");
169 sampleSizeNode.setAttribute("value", String.valueOf(sampleSizeInBits));
170 sampleSizeNode.setAttribute("unit", "bits/sample");
171 audio.appendChild(sampleSizeNode);
172 result = root; // Got some real data...
173 }
174
175 final int channels = af.getChannels();
176 if(channels != AudioSystem.NOT_SPECIFIED)
177 {
178 final Element channelsNode = doc.createElement("channels");
179 channelsNode.setAttribute("value", String.valueOf(channels));
180 audio.appendChild(channelsNode);
181 result = root; // Got some real data...
182 }
183
184 final int frameSize = af.getFrameSize();
185 if(frameSize != AudioSystem.NOT_SPECIFIED)
186 {
187 final Element frameSizeNode = doc.createElement("frameSize");
188 frameSizeNode.setAttribute("value", String.valueOf(frameSize));
189 frameSizeNode.setAttribute("unit", "bytes/frame");
190 audio.appendChild(frameSizeNode);
191 result = root; // Got some real data...
192 }
193
194 final float frameRate = af.getFrameRate();
195 if((Math.abs(sampleRate - frameRate) > 0.00001) &&
196 (frameRate != AudioSystem.NOT_SPECIFIED))
197 {
198 final Element frameRateNode = doc.createElement("frameRate");
199 frameRateNode.setAttribute("value", TextUtils.escapeHTMLMetaChars(String.valueOf(frameRate)));
200 frameRateNode.setAttribute("unit", "frames/second");
201 audio.appendChild(frameRateNode);
202 result = root; // Got some real data...
203 }
204
205 if(((encoding.equals(AudioFormat.Encoding.PCM_SIGNED)
206 || encoding.equals(AudioFormat.Encoding.PCM_UNSIGNED)) &&
207 (sampleSizeInBits > 8))
208 || (sampleSizeInBits == AudioSystem.NOT_SPECIFIED))
209 {
210 final Element endianNode = doc.createElement("bigEndian");
211 endianNode.setAttribute("value", String.valueOf(af.isBigEndian()));
212 audio.appendChild(endianNode);
213 result = root; // Got some real data...
214 }
215
216 final Map<String,Object> aPropMap = af.properties();
217 if((aPropMap != null) && (!aPropMap.isEmpty()))
218 {
219 final Element propsNode = doc.createElement("properties");
220 // Insert all the properties.
221 for(final String key : aPropMap.keySet())
222 {
223 final Element propNode = doc.createElement("property");
224 propNode.setAttribute("name", TextUtils.escapeHTMLMetaChars(key));
225 propNode.setAttribute("value", TextUtils.escapeHTMLMetaChars(String.valueOf(aPropMap.get(key))));
226 propsNode.appendChild(propNode);
227 }
228 audio.appendChild(propsNode);
229 result = root; // Got some real data...
230 }
231 }
232 catch(final ParserConfigurationException e)
233 {
234 e.printStackTrace();
235 return(null); // Cannot build document...
236 }
237
238 return(result);
239 }
240 }