Skip to content

Commit 3220b29

Browse files
authored
Merge pull request #2496 from capdevon/capdevon-WAVLoader
Feat: WAVLoader: Ensures clean instance state, optimization + javadoc
2 parents 43c3111 + 1e311f7 commit 3220b29

File tree

1 file changed

+153
-72
lines changed

1 file changed

+153
-72
lines changed

jme3-core/src/plugins/java/com/jme3/audio/plugins/WAVLoader.java

Lines changed: 153 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
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
@@ -47,39 +47,62 @@
4747
import java.util.logging.Level;
4848
import 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+
*/
5059
public 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

Comments
 (0)