@@ -226,11 +226,45 @@ class JUCE_API PatchedWavAudioFormat : public WavAudioFormat {
226226 bool deleteStreamIfOpeningFails) override {
227227 auto streamStartPos = sourceStream->getPosition ();
228228
229- // Helper to delegate to the parent WavAudioFormat implementation
230- auto useDefaultReader = [&]() {
231- sourceStream->setPosition (streamStartPos);
232- return WavAudioFormat::createReaderFor (sourceStream,
233- deleteStreamIfOpeningFails);
229+ auto useDefaultReader = [&]() -> AudioFormatReader * {
230+ if (!sourceStream->setPosition (streamStartPos)) {
231+ if (deleteStreamIfOpeningFails)
232+ delete sourceStream;
233+ return nullptr ;
234+ }
235+ const auto layout = scanWavLayout (*sourceStream, streamStartPos);
236+ // Let JUCE establish the final stream state, including native fallback
237+ // behavior for file-like objects whose seeks do not always succeed.
238+ if (!sourceStream->setPosition (streamStartPos)) {
239+ if (deleteStreamIfOpeningFails)
240+ delete sourceStream;
241+ return nullptr ;
242+ }
243+ std::unique_ptr<AudioFormatReader> reader (WavAudioFormat::createReaderFor (
244+ sourceStream, deleteStreamIfOpeningFails));
245+ // The default factory can delegate Ogg-in-WAV to a compressed reader.
246+ // Only JUCE's native WAV reader has the byte stride described below.
247+ if (reader && reader->getFormatName () == getFormatName ()) {
248+ // JUCE validates extensible GUIDs and normalizes legacy bit depths.
249+ // Use that validated depth for float32 eligibility, preserving the
250+ // existing float64 and unsupported-float dispatch paths.
251+ const bool canCapLength =
252+ layout.format == (uint16)WavFormatTag::PCM ||
253+ (layout.format == (uint16)WavFormatTag::IEEEFloat &&
254+ reader->bitsPerSample == 32 ) ||
255+ (layout.format == (uint16)WavFormatTag::Extensible &&
256+ (!reader->usesFloatingPointData || reader->bitsPerSample == 32 ));
257+ if (canCapLength && layout.dataOffset >= 0 &&
258+ layout.bytesPerFrame > 0 ) {
259+ const auto available =
260+ std::max<int64>(0 , layout.totalLength - layout.dataOffset );
261+ reader->lengthInSamples = std::min (reader->lengthInSamples ,
262+ available / layout.bytesPerFrame );
263+ }
264+ }
265+ // Null readers may already have deleted the stream. Callback errors stay
266+ // pending for ReadableAudioFile's existing Python exception propagation.
267+ return reader.release ();
234268 };
235269
236270 // Read the RIFF header
@@ -311,6 +345,123 @@ class JUCE_API PatchedWavAudioFormat : public WavAudioFormat {
311345 }
312346
313347private:
348+ struct WavDataLayout {
349+ int64 totalLength = -1 ;
350+ int64 dataOffset = -1 ;
351+ int64 bytesPerFrame = 0 ;
352+ unsigned short format = 0 ;
353+ unsigned int bitsPerSample = 0 ;
354+ };
355+
356+ // This is a best-effort lookup, not a second format validator. An unfamiliar
357+ // or incomplete layout leaves JUCE's result unchanged rather than rejecting
358+ // a file that the original reader could open.
359+ static WavDataLayout scanWavLayout (InputStream &stream, int64 start) {
360+ static_assert (JUCE_VERSION == 0x060104 ,
361+ " Review WAV traversal and decoder stride when updating JUCE" );
362+ WavDataLayout layout;
363+ layout.totalLength = stream.getTotalLength ();
364+ if (layout.totalLength < 0 || start < 0 || !stream.setPosition (start))
365+ return {};
366+
367+ uint8 header[12 ];
368+ if (stream.read (header, sizeof (header)) != sizeof (header) ||
369+ (int )ByteOrder::littleEndianInt (header + 8 ) != chunkName (" WAVE" ))
370+ return {};
371+ const auto container = (int )ByteOrder::littleEndianInt (header);
372+ const bool isRF64 = container == chunkName (" RF64" );
373+ if (!isRF64 && container != chunkName (" RIFF" ))
374+ return {};
375+
376+ uint64 riffSize = ByteOrder::littleEndianInt (header + 4 );
377+ int64 rf64DataSize = 0 ;
378+ if (isRF64) {
379+ uint8 ds64[24 ];
380+ if (stream.read (ds64, sizeof (ds64)) != sizeof (ds64) ||
381+ (int )ByteOrder::littleEndianInt (ds64) != chunkName (" ds64" ))
382+ return {};
383+ const auto size = ByteOrder::littleEndianInt (ds64 + 4 );
384+ riffSize = ByteOrder::littleEndianInt64 (ds64 + 8 );
385+ const auto dataSize = ByteOrder::littleEndianInt64 (ds64 + 16 );
386+ if (size < 28 || dataSize > (uint64)std::numeric_limits<int64>::max ())
387+ return {};
388+ rf64DataSize = (int64)dataSize;
389+ const int64 remaining = (int64)size + (size & 1 ) - 16 ;
390+ if (remaining > layout.totalLength - stream.getPosition () ||
391+ !stream.setPosition (stream.getPosition () + remaining))
392+ return {};
393+ }
394+ if (start > std::numeric_limits<int64>::max () - 8 ||
395+ riffSize > (uint64)(std::numeric_limits<int64>::max () - start - 8 ))
396+ return {};
397+ const auto scanEnd =
398+ std::min (layout.totalLength , start + 8 + (int64)riffSize);
399+
400+ while (stream.getPosition () < scanEnd) {
401+ const auto chunkStart = stream.getPosition ();
402+ uint8 chunk[8 ];
403+ // JUCE tests only the chunk start against RIFF end. The header itself
404+ // can cross an understated container boundary if physically present.
405+ if (stream.read (chunk, sizeof (chunk)) != sizeof (chunk))
406+ return {};
407+ const auto type = (int )ByteOrder::littleEndianInt (chunk);
408+ const auto length = ByteOrder::littleEndianInt (chunk + 4 );
409+ const auto payload = stream.getPosition ();
410+ int64 payloadLength = length;
411+ if (payload <= chunkStart)
412+ return {};
413+
414+ if (type == chunkName (" fmt " )) {
415+ uint8 fmt[16 ];
416+ // JUCE reads these fields even when the declared fmt size is short.
417+ // Follow that layout without imposing stricter header validation.
418+ if (stream.read (fmt, sizeof (fmt)) != sizeof (fmt))
419+ return {};
420+ layout.format = ByteOrder::littleEndianShort (fmt);
421+ const auto channels =
422+ (unsigned int )(short )ByteOrder::littleEndianShort (fmt + 2 );
423+ const auto sampleRate = (int32)ByteOrder::littleEndianInt (fmt + 4 );
424+ const auto byteRate = (int32)ByteOrder::littleEndianInt (fmt + 8 );
425+ layout.bitsPerSample =
426+ (unsigned int )(int )(short )ByteOrder::littleEndianShort (fmt + 14 );
427+ // Mirror JUCE 6.1.4, including legacy bit-depth normalization. The
428+ // blockAlign field does not determine the decoder's byte stride.
429+ layout.bytesPerFrame = layout.bitsPerSample > 64 && sampleRate != 0
430+ ? (int64)byteRate / sampleRate
431+ : (int )(channels * layout.bitsPerSample / 8 );
432+ } else if (type == chunkName (" data" )) {
433+ // JUCE selects the last data chunk it visits, using its current format.
434+ // Retain the final decoding stride even if another fmt follows data.
435+ layout.dataOffset = payload;
436+ if (isRF64 && rf64DataSize > 0 )
437+ payloadLength = rf64DataSize;
438+ } else if (length == 0 ) {
439+ // JUCE stops at zero-sized unknown chunks, but continues past metadata
440+ // it recognises. Match that distinction before selecting a later data.
441+ const int metadata[] = {
442+ chunkName (" bext" ), chunkName (" smpl" ), chunkName (" inst" ),
443+ chunkName (" INST" ), chunkName (" cue " ), chunkName (" axml" ),
444+ chunkName (" LIST" ), chunkName (" acid" ), chunkName (" Trkn" )};
445+ if (std::find (std::begin (metadata), std::end (metadata), type) ==
446+ std::end (metadata))
447+ break ;
448+ }
449+
450+ // A short final payload (including metadata or a missing pad byte) is
451+ // accepted or rejected by JUCE, not by this lookup. Oversized audio uses
452+ // the physical tail below; an understated RIFF end never expands length.
453+ if (payloadLength > scanEnd - payload)
454+ break ;
455+ const auto end = payload + payloadLength;
456+ if ((payloadLength & 1 ) != 0 && end == scanEnd)
457+ break ;
458+ const auto next = end + (payloadLength & 1 );
459+ if (!stream.setPosition (next) || stream.getPosition () != next)
460+ return {};
461+ }
462+ return layout;
463+ }
464+
314465 /* *
315466 * Creates an MP3 reader for a WAV file containing MP3-compressed audio data.
316467 * Finds the data chunk and wraps it in a SubregionStream for the MP3 decoder.
0 commit comments