11/*
2- * Copyright (c) 2009-2021 jMonkeyEngine
2+ * Copyright (c) 2009-2025 jMonkeyEngine
33 * All rights reserved.
44 *
55 * Redistribution and use in source and binary forms, with or without
4747import java .util .logging .Level ;
4848import java .util .logging .Logger ;
4949
50+ /**
51+ * An {@code AssetLoader} for loading WAV audio files.
52+ * This loader supports PCM (Pulse Code Modulation) WAV files,
53+ * both as in-memory {@link AudioBuffer}s and streaming {@link AudioStream}s.
54+ * It handles 8-bit and 16-bit audio formats.
55+ *
56+ * <p>The WAV file format consists of chunks. This loader specifically parses
57+ * the 'RIFF', 'WAVE', 'fmt ', and 'data' chunks.
58+ */
5059public class WAVLoader implements AssetLoader {
5160
5261 private static final Logger logger = Logger .getLogger (WAVLoader .class .getName ());
5362
54- // all these are in big endian
63+ // RIFF chunk identifiers (Big-Endian representation of ASCII characters)
5564 private static final int i_RIFF = 0x46464952 ;
5665 private static final int i_WAVE = 0x45564157 ;
5766 private static final int i_fmt = 0x20746D66 ;
5867 private static final int i_data = 0x61746164 ;
5968
60- private boolean readStream = false ;
61-
62- private AudioBuffer audioBuffer ;
63- private AudioStream audioStream ;
64- private AudioData audioData ;
69+ /**
70+ * The number of bytes per second for the audio data, calculated from the WAV header.
71+ * Used to determine the duration of the audio.
72+ */
6573 private int bytesPerSec ;
74+ /**
75+ * The duration of the audio in seconds.
76+ */
6677 private float duration ;
67-
78+ /**
79+ * The input stream for reading the WAV file data.
80+ */
6881 private ResettableInputStream in ;
69- private int inOffset = 0 ;
70-
82+
83+ /**
84+ * A custom {@link InputStream} extension that handles little-endian byte
85+ * reading and provides seek capabilities for streaming audio by reopening
86+ * and skipping the input stream.
87+ */
7188 private static class ResettableInputStream extends LittleEndien implements SeekableStream {
7289
73- final private AssetInfo info ;
90+ private final AssetInfo info ;
7491 private int resetOffset = 0 ;
7592
7693 public ResettableInputStream (AssetInfo info , InputStream in ) {
7794 super (in );
7895 this .info = info ;
7996 }
80-
81- public void setResetOffset (int resetOffset ) {
82- this .resetOffset = resetOffset ;
97+
98+ /**
99+ * Sets the offset from the beginning of the file to reset the stream to.
100+ * This is typically the start of the audio data chunk.
101+ *
102+ * @param offset The byte offset to reset to.
103+ */
104+ public void setResetOffset (int offset ) {
105+ this .resetOffset = offset ;
83106 }
84107
85108 @ Override
@@ -95,141 +118,199 @@ public void setTime(float time) {
95118 // Resource could have gotten lost, etc.
96119 try {
97120 newStream .close ();
98- } catch (IOException ex2 ) {
121+ } catch (IOException ignored ) {
99122 }
100123 throw new RuntimeException (ex );
101124 }
102125 }
103126 }
104127
105- private void readFormatChunk (int size ) throws IOException {
128+ /**
129+ * Reads and parses the 'fmt ' (format) chunk of the WAV file.
130+ * This chunk contains information about the audio format such as
131+ * compression, channels, sample rate, bits per sample, etc.
132+ *
133+ * @param chunkSize The size of the 'fmt ' chunk in bytes.
134+ * @param audioData The {@link AudioData} object to set the format information on.
135+ * @throws IOException if the file is not a supported PCM WAV, or if format
136+ * parameters are invalid.
137+ */
138+ private void readFormatChunk (int chunkSize , AudioData audioData ) throws IOException {
106139 // if other compressions are supported, size doesn't have to be 16
107140// if (size != 16)
108141// logger.warning("Expected size of format chunk to be 16");
109142
110143 int compression = in .readShort ();
111- if (compression != 1 ){
144+ if (compression != 1 ) { // 1 = PCM (Pulse Code Modulation)
112145 throw new IOException ("WAV Loader only supports PCM wave files" );
113146 }
114147
115- int channels = in .readShort ();
148+ int numChannels = in .readShort ();
116149 int sampleRate = in .readInt ();
150+ bytesPerSec = in .readInt (); // Average bytes per second
117151
118- bytesPerSec = in .readInt (); // used to calculate duration
119-
120- int bytesPerSample = in .readShort ();
152+ int bytesPerSample = in .readShort (); // Bytes per sample block (channels * bytesPerSample)
121153 int bitsPerSample = in .readShort ();
122154
123- int expectedBytesPerSec = (bitsPerSample * channels * sampleRate ) / 8 ;
124- if (expectedBytesPerSec != bytesPerSec ){
155+ int expectedBytesPerSec = (bitsPerSample * numChannels * sampleRate ) / 8 ;
156+ if (expectedBytesPerSec != bytesPerSec ) {
125157 logger .log (Level .WARNING , "Expected {0} bytes per second, got {1}" ,
126158 new Object []{expectedBytesPerSec , bytesPerSec });
127159 }
128-
160+
129161 if (bitsPerSample != 8 && bitsPerSample != 16 )
130162 throw new IOException ("Only 8 and 16 bits per sample are supported!" );
131163
132- if ( (bitsPerSample / 8 ) * channels != bytesPerSample )
164+ if ((bitsPerSample / 8 ) * numChannels != bytesPerSample )
133165 throw new IOException ("Invalid bytes per sample value" );
134166
135167 if (bytesPerSample * sampleRate != bytesPerSec )
136168 throw new IOException ("Invalid bytes per second value" );
137169
138- audioData .setupFormat (channels , bitsPerSample , sampleRate );
170+ audioData .setupFormat (numChannels , bitsPerSample , sampleRate );
139171
140- int remaining = size - 16 ;
141- if (remaining > 0 ){
142- in .skipBytes (remaining );
172+ // Skip any extra parameters in the format chunk (e.g., for non-PCM formats)
173+ int remainingChunkBytes = chunkSize - 16 ;
174+ if (remainingChunkBytes > 0 ) {
175+ in .skipBytes (remainingChunkBytes );
143176 }
144177 }
145178
146- private void readDataChunkForBuffer (int len ) throws IOException {
147- ByteBuffer data = BufferUtils .createByteBuffer (len );
148- byte [] buf = new byte [512 ];
179+ /**
180+ * Reads the 'data' chunk for an {@link AudioBuffer}. This involves loading
181+ * the entire audio data into a {@link ByteBuffer} in memory.
182+ *
183+ * @param dataChunkSize The size of the 'data' chunk in bytes.
184+ * @param audioBuffer The {@link AudioBuffer} to update with the loaded data.
185+ * @throws IOException if an error occurs while reading the data.
186+ */
187+ private void readDataChunkForBuffer (int dataChunkSize , AudioBuffer audioBuffer ) throws IOException {
188+ ByteBuffer data = BufferUtils .createByteBuffer (dataChunkSize );
189+ byte [] buf = new byte [1024 ]; // Use a larger buffer for efficiency
149190 int read = 0 ;
150- while ( (read = in .read (buf )) > 0 ){
151- data .put (buf , 0 , Math .min (read , data .remaining ()) );
191+ while ((read = in .read (buf )) > 0 ) {
192+ data .put (buf , 0 , Math .min (read , data .remaining ()));
152193 }
153194 data .flip ();
154195 audioBuffer .updateData (data );
155196 in .close ();
156197 }
157198
158- private void readDataChunkForStream (int offset , int len ) throws IOException {
159- in .setResetOffset (offset );
199+ /**
200+ * Configures the {@link AudioStream} to stream data from the 'data' chunk.
201+ * This involves setting the reset offset for seeking and passing the
202+ * input stream and duration to the {@link AudioStream}.
203+ *
204+ * @param dataChunkOffset The byte offset from the start of the file where the 'data' chunk begins.
205+ * @param dataChunkSize The size of the 'data' chunk in bytes.
206+ * @param audioStream The {@link AudioStream} to configure.
207+ */
208+ private void readDataChunkForStream (int dataChunkOffset , int dataChunkSize , AudioStream audioStream ) {
209+ in .setResetOffset (dataChunkOffset );
160210 audioStream .updateData (in , duration );
161211 }
162212
163- private AudioData load (AssetInfo info , InputStream inputStream , boolean stream ) throws IOException {
213+ /**
214+ * Main loading logic for WAV files. This method parses the RIFF, WAVE, fmt,
215+ * and data chunks to extract audio information and data.
216+ *
217+ * @param info The {@link AssetInfo} for the WAV file.
218+ * @param inputStream The initial {@link InputStream} opened for the asset.
219+ * @param stream A boolean indicating whether the audio should be loaded
220+ * as a stream (true) or an in-memory buffer (false).
221+ * @return The loaded {@link AudioData} (either {@link AudioBuffer} or {@link AudioStream}).
222+ * @throws IOException if the file is not a valid WAV, or if any I/O error occurs.
223+ */
224+ private AudioData load (AssetInfo info , InputStream inputStream , boolean stream ) throws IOException {
164225 this .in = new ResettableInputStream (info , inputStream );
165- inOffset = 0 ;
166-
167- int sig = in .readInt ();
168- if (sig != i_RIFF )
226+ int inOffset = 0 ;
227+
228+ // Read RIFF chunk
229+ int riffId = in .readInt ();
230+ if (riffId != i_RIFF ) {
169231 throw new IOException ("File is not a WAVE file" );
170-
171- // skip size
232+ }
233+
234+ // Skip RIFF chunk size
172235 in .readInt ();
173- if (in .readInt () != i_WAVE )
236+
237+ int waveId = in .readInt ();
238+ if (waveId != i_WAVE )
174239 throw new IOException ("WAVE File does not contain audio" );
175240
176- inOffset += 4 * 3 ;
177-
178- readStream = stream ;
179- if (readStream ){
241+ inOffset += 4 * 3 ; // RIFF_ID + ChunkSize + WAVE_ID
242+
243+ AudioData audioData ;
244+ AudioBuffer audioBuffer = null ;
245+ AudioStream audioStream = null ;
246+
247+ if (stream ) {
180248 audioStream = new AudioStream ();
181249 audioData = audioStream ;
182- }else {
250+ } else {
183251 audioBuffer = new AudioBuffer ();
184252 audioData = audioBuffer ;
185253 }
186254
187255 while (true ) {
188- int type = in .readInt ();
189- int len = in .readInt ();
190-
191- inOffset += 4 * 2 ;
256+ int chunkType = in .readInt ();
257+ int chunkSize = in .readInt ();
258+
259+ inOffset += 4 * 2 ; // ChunkType + ChunkSize
192260
193- switch (type ) {
261+ switch (chunkType ) {
194262 case i_fmt :
195- readFormatChunk (len );
196- inOffset += len ;
263+ readFormatChunk (chunkSize , audioData );
264+ inOffset += chunkSize ;
197265 break ;
198266 case i_data :
199267 // Compute duration based on data chunk size
200- duration = len / bytesPerSec ;
268+ duration = ( float ) ( chunkSize / bytesPerSec ) ;
201269
202- if (readStream ) {
203- readDataChunkForStream (inOffset , len );
270+ if (stream ) {
271+ readDataChunkForStream (inOffset , chunkSize , audioStream );
204272 } else {
205- readDataChunkForBuffer (len );
273+ readDataChunkForBuffer (chunkSize , audioBuffer );
206274 }
207275 return audioData ;
208276 default :
209- int skipped = in .skipBytes (len );
210- if (skipped <= 0 ) {
277+ // Skip unknown chunks
278+ int skippedBytes = in .skipBytes (chunkSize );
279+ if (skippedBytes <= 0 ) {
280+ logger .log (Level .WARNING , "Reached end of stream prematurely while skipping unknown chunk of size {0}. Asset: {1}" ,
281+ new Object []{chunkSize , info .getKey ().getName ()});
211282 return null ;
212283 }
213- inOffset += skipped ;
284+ inOffset += skippedBytes ;
214285 break ;
215286 }
216287 }
217288 }
218289
219290 @ Override
220291 public Object load (AssetInfo info ) throws IOException {
221- AudioData data ;
222- InputStream inputStream = null ;
292+ InputStream is = null ;
223293 try {
224- inputStream = info .openStream ();
225- data = load (info , inputStream , ((AudioKey )info .getKey ()).isStream ());
226- if (data instanceof AudioStream ){
227- inputStream = null ;
294+ is = info .openStream ();
295+ boolean streamAudio = ((AudioKey ) info .getKey ()).isStream ();
296+ AudioData loadedData = load (info , is , streamAudio );
297+
298+ // If it's an AudioStream, the internal inputStream is managed by the stream itself
299+ // and should not be closed here.
300+ if (loadedData instanceof AudioStream ) {
301+ // Prevent closing in finally block
302+ is = null ;
228303 }
229- return data ;
304+ return loadedData ;
230305 } finally {
231- if (inputStream != null ){
232- inputStream .close ();
306+ // Nullify/reset instance variables to ensure the loader instance is clean
307+ // for the next load operation.
308+ in = null ;
309+ bytesPerSec = 0 ;
310+ duration = 0.0f ;
311+
312+ if (is != null ) {
313+ is .close ();
233314 }
234315 }
235316 }
0 commit comments