Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## 2.1.0 (#unreleased)

- Feature [#468](https://github.com/SimformSolutionsPvtLtd/audio_waveforms/pull/468) - Add macOS support
- Fixed [#113](https://github.com/SimformSolutionsPvtLtd/audio_waveforms/issues/113) - Play audio files with mislabeled extensions on iOS/macOS

## 2.0.2

Expand Down
7 changes: 7 additions & 0 deletions doc/documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,13 @@ await playerController.preparePlayer(
Currently, playing remote audio files directly isn't supported. You will need to download the file
first, then play it locally.

### Mislabeled File Extensions (iOS/macOS)

iOS/macOS pick the audio parser from the file extension, so a file whose extension doesn't match
its real container (e.g. an Android AAC-in-MP4 recording named `.mp3`/`.aac`) fails to load.
`preparePlayer` detects this from the file's magic bytes and handles it automatically — no action
needed.

## Start Playing

Start playing the audio:
Expand Down
64 changes: 64 additions & 0 deletions lib/src/base/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -204,3 +204,67 @@ enum WaveformRenderMode {

/// Checks if the current platform is iOS or macOS.
bool get isIosOrMacOS => Platform.isIOS || Platform.isMacOS;

/// Sniffs an audio file's container from its leading magic bytes and returns
/// the file extension AVFoundation needs to pick the right parser, or `null`
/// when the container isn't recognised.
Future<String?> sniffAudioExtension(String path) async {
RandomAccessFile? raf;
try {
raf = await File(path).open();
final b = await raf.read(12);
if (b.length < 4) return null;
String ascii(int start, int end) =>
end <= b.length ? String.fromCharCodes(b.sublist(start, end)) : '';
// MP4 / M4A container: bytes 4..8 == "ftyp" (covers AAC-in-MP4).
if (ascii(4, 8) == 'ftyp') return 'm4a';
if (ascii(0, 4) == 'RIFF' && ascii(8, 12) == 'WAVE') return 'wav';
if (ascii(0, 4) == 'fLaC') return 'flac';
if (ascii(0, 4) == 'OggS') return 'ogg';
if (ascii(0, 3) == 'ID3') return 'mp3';
// Raw frame sync: 0xFFFx = ADTS AAC, 0xFFEx = MP3.
if (b[0] == 0xFF) {
if ((b[1] & 0xF6) == 0xF0) return 'aac';
if ((b[1] & 0xE0) == 0xE0) return 'mp3';
}
return null;
} catch (_) {
return null;
} finally {
await raf?.close();
}
}

/// On iOS/macOS, AVFoundation routes strictly by file extension. Android can
/// record AAC-in-MP4 yet name the file `.mp3`/`.aac` (issue #113), which then
/// fails to load or silently mis-parses (e.g. an 8ms bogus duration). When the
/// extension disagrees with the container sniffed from magic bytes, this returns
/// a temp symlink carrying the correct extension so the right parser is chosen;
/// otherwise it returns [path] unchanged. No-op on other platforms.
Future<String> normalizedAudioPath(String path) async {
if (!isIosOrMacOS) return path;
final ext = await sniffAudioExtension(path);
if (ext == null) return path;
final dot = path.lastIndexOf('.');
final currentExt = dot == -1 ? '' : path.substring(dot + 1).toLowerCase();
if (currentExt == ext) return path;
try {
final link =
Link('${Directory.systemTemp.path}/awf_sniff_${path.hashCode}.$ext');
final type = await FileSystemEntity.type(link.path);
if (type == FileSystemEntityType.link) {
// Reuse only when it already targets this exact file; otherwise the
// name collided with another path, so re-point it.
if (await link.target() == path) return link.path;
await link.delete();
} else if (type != FileSystemEntityType.notFound) {
// A stale regular file/dir is squatting on the name; clear it so the
// link can be created instead of silently disabling the fix.
await File(link.path).delete();
}
await link.create(path);
return link.path;
} catch (_) {
return path;
}
}
4 changes: 4 additions & 0 deletions lib/src/controllers/player_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ class PlayerController extends ChangeNotifier {
throw ArgumentError('Invalid path format: $path');
}
path = uri.path;
// A wrong extension (e.g. Android AAC-in-MP4 named `.mp3`, issue #113)
// makes AVFoundation pick the wrong parser on iOS/macOS. Redirect to a
// correctly-named symlink when the sniffed container disagrees.
path = await normalizedAudioPath(path);
}
final isPrepared = await AudioWaveformsInterface.instance.preparePlayer(
path: path,
Expand Down