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        }