Skip to content

Commit 408c48c

Browse files
authored
feat: sound audition flow (#122)
* audition rough poc * fix: harden sound audition workflow * refactor: consolidate sound audition state and waveform handling * refactor: make sound audition playback host-agnostic
1 parent 71e04b5 commit 408c48c

39 files changed

Lines changed: 3642 additions & 1100 deletions

ios/Podfile.lock

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,14 @@
11
PODS:
2-
- audio_waveforms (0.0.1):
3-
- Flutter
42
- Flutter (1.0.0)
53

64
DEPENDENCIES:
7-
- audio_waveforms (from `.symlinks/plugins/audio_waveforms/ios`)
85
- Flutter (from `Flutter`)
96

107
EXTERNAL SOURCES:
11-
audio_waveforms:
12-
:path: ".symlinks/plugins/audio_waveforms/ios"
138
Flutter:
149
:path: Flutter
1510

1611
SPEC CHECKSUMS:
17-
audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf
1812
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
1913

2014
PODFILE CHECKSUM: e716c32704b29904c5fe233535ab45faecf8c549

ios/Runner.xcodeproj/project.pbxproj

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,6 @@
209209
97C146EC1CF9000F007C117D /* Resources */,
210210
9705A1C41CF9048500538489 /* Embed Frameworks */,
211211
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
212-
690D4588462089555B03AB06 /* [CP] Embed Pods Frameworks */,
213212
);
214213
buildRules = (
215214
);
@@ -305,23 +304,6 @@
305304
shellPath = /bin/sh;
306305
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
307306
};
308-
690D4588462089555B03AB06 /* [CP] Embed Pods Frameworks */ = {
309-
isa = PBXShellScriptBuildPhase;
310-
buildActionMask = 2147483647;
311-
files = (
312-
);
313-
inputFileListPaths = (
314-
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
315-
);
316-
name = "[CP] Embed Pods Frameworks";
317-
outputFileListPaths = (
318-
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
319-
);
320-
runOnlyForDeploymentPostprocessing = 0;
321-
shellPath = /bin/sh;
322-
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
323-
showEnvVarsInLog = 0;
324-
};
325307
6CBBC3DF6451F183E15B477D /* [CP] Check Pods Manifest.lock */ = {
326308
isa = PBXShellScriptBuildPhase;
327309
buildActionMask = 2147483647;

lib/src/core/l10n/app_localizations.dart

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,12 @@ abstract class AppLocalizations {
166166
/// **'Replace'**
167167
String get buttonReplace;
168168

169+
/// Change which part of the selected sound is used
170+
///
171+
/// In en, this message translates to:
172+
/// **'Adjust clip'**
173+
String get buttonAdjustClip;
174+
169175
/// Mute the selected audio track
170176
///
171177
/// In en, this message translates to:
@@ -1354,6 +1360,18 @@ abstract class AppLocalizations {
13541360
/// **'Select sound'**
13551361
String get titleSelectSound;
13561362

1363+
/// Title for choosing which section of a sound plays over a video
1364+
///
1365+
/// In en, this message translates to:
1366+
/// **'Select sound clip'**
1367+
String get labelSelectSoundClip;
1368+
1369+
/// Instruction below the video editor sound waveform
1370+
///
1371+
/// In en, this message translates to:
1372+
/// **'Drag to choose the part that plays'**
1373+
String get hintDragWaveform;
1374+
13571375
/// Placeholder for sound picker search input
13581376
///
13591377
/// In en, this message translates to:

lib/src/core/l10n/app_localizations_en.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ class AppLocalizationsEn extends AppLocalizations {
4444
@override
4545
String get buttonReplace => 'Replace';
4646

47+
@override
48+
String get buttonAdjustClip => 'Adjust clip';
49+
4750
@override
4851
String get buttonMute => 'Mute';
4952

@@ -679,6 +682,12 @@ class AppLocalizationsEn extends AppLocalizations {
679682
@override
680683
String get titleSelectSound => 'Select sound';
681684

685+
@override
686+
String get labelSelectSoundClip => 'Select sound clip';
687+
688+
@override
689+
String get hintDragWaveform => 'Drag to choose the part that plays';
690+
682691
@override
683692
String get hintSearchSounds => 'Search sounds';
684693

lib/src/core/l10n/intl_en.arb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@
6262
"description": "Replace the selected media track"
6363
},
6464

65+
"buttonAdjustClip": "Adjust clip",
66+
"@buttonAdjustClip": {
67+
"description": "Change which part of the selected sound is used"
68+
},
69+
6570
"buttonMute": "Mute",
6671
"@buttonMute": {
6772
"description": "Mute the selected audio track"
@@ -1103,6 +1108,16 @@
11031108
"description": "Title for the recorder sound picker"
11041109
},
11051110

1111+
"labelSelectSoundClip": "Select sound clip",
1112+
"@labelSelectSoundClip": {
1113+
"description": "Title for choosing which section of a sound plays over a video"
1114+
},
1115+
1116+
"hintDragWaveform": "Drag to choose the part that plays",
1117+
"@hintDragWaveform": {
1118+
"description": "Instruction below the video editor sound waveform"
1119+
},
1120+
11061121
"hintSearchSounds": "Search sounds",
11071122
"@hintSearchSounds": {
11081123
"description": "Placeholder for sound picker search input"
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import 'package:pro_image_editor/pro_image_editor.dart';
2+
3+
Duration audioSelectionDuration({
4+
required Duration audioDuration,
5+
required Duration selectionWindowDuration,
6+
}) {
7+
if (audioDuration <= Duration.zero ||
8+
selectionWindowDuration <= Duration.zero) {
9+
return Duration.zero;
10+
}
11+
return audioDuration < selectionWindowDuration
12+
? audioDuration
13+
: selectionWindowDuration;
14+
}
15+
16+
AudioTrack audioTrackForAuditionRange(
17+
AudioTrack track, {
18+
required TrimDurationSpan playbackSpan,
19+
Duration? sourceStart,
20+
}) {
21+
final playbackDuration = playbackSpan.end - playbackSpan.start;
22+
final selectionDuration = audioSelectionDuration(
23+
audioDuration: track.duration,
24+
selectionWindowDuration: playbackDuration,
25+
);
26+
final maximumStart = track.duration - selectionDuration;
27+
var normalizedSourceStart =
28+
sourceStart ?? track.audioStartTime ?? Duration.zero;
29+
if (normalizedSourceStart < Duration.zero) {
30+
normalizedSourceStart = Duration.zero;
31+
}
32+
if (normalizedSourceStart > maximumStart) {
33+
normalizedSourceStart = maximumStart;
34+
}
35+
return track.copyWith(
36+
audioStartTime: normalizedSourceStart,
37+
audioEndTime: normalizedSourceStart + selectionDuration,
38+
startTime: playbackSpan.start,
39+
endTime: playbackSpan.end,
40+
loop: track.duration < playbackDuration,
41+
);
42+
}
43+
44+
TrimDurationSpan audioTrackPreviewRange({
45+
required AudioTrack track,
46+
required Duration hostStart,
47+
required Duration hostEnd,
48+
}) {
49+
final trackStart = track.startTime ?? hostStart;
50+
final trackEnd = track.endTime ?? hostEnd;
51+
final previewStart = trackStart > hostStart ? trackStart : hostStart;
52+
final previewEnd = trackEnd < hostEnd ? trackEnd : hostEnd;
53+
if (previewEnd <= previewStart) {
54+
return TrimDurationSpan(start: hostStart, end: hostEnd);
55+
}
56+
return TrimDurationSpan(start: previewStart, end: previewEnd);
57+
}
58+
59+
double audioRangePlaybackProgress({
60+
required Duration position,
61+
required Duration rangeStart,
62+
required Duration rangeEnd,
63+
}) {
64+
final rangeDuration = rangeEnd - rangeStart;
65+
if (rangeDuration <= Duration.zero) return 0;
66+
return ((position - rangeStart).inMicroseconds / rangeDuration.inMicroseconds)
67+
.clamp(0.0, 1.0)
68+
.toDouble();
69+
}
70+
71+
Duration? audioRangeLoopTarget({
72+
required bool isPlaybackArmed,
73+
required bool isPlaybackCompleted,
74+
required Duration position,
75+
required TrimDurationSpan range,
76+
}) {
77+
final isOutsideRange = position < range.start || position >= range.end;
78+
if (!isPlaybackArmed || (!isPlaybackCompleted && !isOutsideRange)) {
79+
return null;
80+
}
81+
return range.start;
82+
}

lib/src/core/pro_video_editor/models/sound_audio_track.dart

Lines changed: 64 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,10 @@ String decodeSoundTrackAudioFileExtension(String? encoded) {
7777
if (encoded == null) return _fallbackAudioFileExtension;
7878
try {
7979
final map = jsonDecode(encoded) as Map<String, dynamic>;
80-
return _normalizeAudioFileExtension(map['audioFileExtension'] as String?);
80+
return _resolveAudioFormat(
81+
extension: map['audioFileExtension'] as String?,
82+
mimeType: map['audioMimeType'] as String?,
83+
).extension;
8184
} catch (_) {
8285
return _fallbackAudioFileExtension;
8386
}
@@ -87,10 +90,10 @@ String decodeSoundTrackAudioMimeType(String? encoded) {
8790
if (encoded == null) return 'audio/mpeg';
8891
try {
8992
final map = jsonDecode(encoded) as Map<String, dynamic>;
90-
return _normalizeAudioMimeType(
91-
map['audioMimeType'] as String?,
93+
return _resolveAudioFormat(
9294
extension: map['audioFileExtension'] as String?,
93-
);
95+
mimeType: map['audioMimeType'] as String?,
96+
).mimeType;
9497
} catch (_) {
9598
return 'audio/mpeg';
9699
}
@@ -99,27 +102,27 @@ String decodeSoundTrackAudioMimeType(String? encoded) {
99102
String audioFileExtension(AudioView audio) {
100103
final record = audio.localRecord;
101104
if (record is PlyrTrackRecord) {
102-
return _normalizeAudioFileExtension(
103-
record.fileType,
105+
return _resolveAudioFormat(
106+
extension: record.fileType,
104107
mimeType: record.audioBlob?.mimeType,
105-
);
108+
).extension;
106109
}
107110
if (record is AudioRecord) {
108-
return _normalizeAudioFileExtension(null, mimeType: record.sound.mimeType);
111+
return _resolveAudioFormat(mimeType: record.sound.mimeType).extension;
109112
}
110113
return _fallbackAudioFileExtension;
111114
}
112115

113116
String audioMimeType(AudioView audio) {
114117
final record = audio.localRecord;
115118
if (record is PlyrTrackRecord) {
116-
return _normalizeAudioMimeType(
117-
record.audioBlob?.mimeType,
119+
return _resolveAudioFormat(
118120
extension: record.fileType,
119-
);
121+
mimeType: record.audioBlob?.mimeType,
122+
).mimeType;
120123
}
121124
if (record is AudioRecord) {
122-
return _normalizeAudioMimeType(record.sound.mimeType);
125+
return _resolveAudioFormat(mimeType: record.sound.mimeType).mimeType;
123126
}
124127
return 'audio/mpeg';
125128
}
@@ -172,55 +175,58 @@ bool _isSparkMediaSoundUrl(String url) {
172175
uri.pathSegments.first == 'sound';
173176
}
174177

175-
String _normalizeAudioFileExtension(String? value, {String? mimeType}) {
176-
final extension = value?.trim().toLowerCase().replaceFirst('.', '');
177-
if (extension != null && extension.isNotEmpty) {
178-
final normalized = switch (extension) {
179-
'mpeg' => 'mp3',
180-
'mp4' => 'm4a',
181-
'x-m4a' => 'm4a',
182-
'x-wav' => 'wav',
183-
_ => extension.replaceAll(RegExp(r'[^a-z0-9]'), ''),
184-
};
185-
if (normalized.isNotEmpty) return normalized;
186-
}
187-
188-
final subtype = mimeType?.split('/').last.trim().toLowerCase();
189-
return switch (subtype) {
190-
'mpeg' => 'mp3',
191-
'mp3' => 'mp3',
192-
'mp4' => 'm4a',
193-
'm4a' => 'm4a',
194-
'x-m4a' => 'm4a',
195-
'aac' => 'aac',
196-
'vnd.wave' => 'wav',
197-
'wave' => 'wav',
198-
'wav' => 'wav',
199-
'x-wav' => 'wav',
200-
'flac' => 'flac',
201-
'ogg' => 'ogg',
202-
_ => _fallbackAudioFileExtension,
178+
_AudioFormat _resolveAudioFormat({String? extension, String? mimeType}) {
179+
final normalizedMime = mimeType?.trim().toLowerCase();
180+
final mimeFormat = switch (normalizedMime) {
181+
'audio/mpeg' || 'audio/mp3' => const _AudioFormat('mp3', 'audio/mpeg'),
182+
'audio/mp4' ||
183+
'audio/m4a' ||
184+
'audio/x-m4a' => const _AudioFormat('m4a', 'audio/mp4'),
185+
'audio/aac' => const _AudioFormat('aac', 'audio/aac'),
186+
'audio/vnd.wave' ||
187+
'audio/wave' ||
188+
'audio/wav' ||
189+
'audio/x-wav' => const _AudioFormat('wav', 'audio/wav'),
190+
'audio/flac' || 'audio/x-flac' => const _AudioFormat('flac', 'audio/flac'),
191+
'audio/ogg' => const _AudioFormat('ogg', 'audio/ogg'),
192+
_ => null,
203193
};
204-
}
194+
if (mimeFormat != null) return mimeFormat;
195+
196+
final normalizedExtension = extension
197+
?.trim()
198+
.toLowerCase()
199+
.replaceFirst('.', '')
200+
.replaceAll(RegExp(r'[^a-z0-9]'), '');
201+
final extensionFormat = switch (normalizedExtension) {
202+
'mpeg' || 'mp3' => const _AudioFormat('mp3', 'audio/mpeg'),
203+
'mp4' || 'm4a' || 'xm4a' => const _AudioFormat('m4a', 'audio/mp4'),
204+
'aac' => const _AudioFormat('aac', 'audio/aac'),
205+
'vndwave' ||
206+
'wave' ||
207+
'wav' ||
208+
'xwav' => const _AudioFormat('wav', 'audio/wav'),
209+
'flac' || 'xflac' => const _AudioFormat('flac', 'audio/flac'),
210+
'ogg' => const _AudioFormat('ogg', 'audio/ogg'),
211+
_ => null,
212+
};
213+
if (extensionFormat != null) return extensionFormat;
205214

206-
String _normalizeAudioMimeType(String? mimeType, {String? extension}) {
207-
final normalizedMime = mimeType?.trim().toLowerCase();
208215
if (normalizedMime != null && normalizedMime.startsWith('audio/')) {
209-
return switch (normalizedMime) {
210-
'audio/x-m4a' => 'audio/mp4',
211-
'audio/vnd.wave' => 'audio/wav',
212-
'audio/wave' => 'audio/wav',
213-
'audio/x-wav' => 'audio/wav',
214-
_ => normalizedMime,
215-
};
216+
final subtype = normalizedMime
217+
.substring('audio/'.length)
218+
.replaceAll(RegExp(r'[^a-z0-9]'), '');
219+
if (subtype.isNotEmpty) return _AudioFormat(subtype, normalizedMime);
216220
}
221+
if (normalizedExtension != null && normalizedExtension.isNotEmpty) {
222+
return _AudioFormat(normalizedExtension, 'audio/$normalizedExtension');
223+
}
224+
return const _AudioFormat(_fallbackAudioFileExtension, 'audio/mpeg');
225+
}
217226

218-
return switch (_normalizeAudioFileExtension(extension)) {
219-
'm4a' => 'audio/mp4',
220-
'aac' => 'audio/aac',
221-
'wav' => 'audio/wav',
222-
'flac' => 'audio/flac',
223-
'ogg' => 'audio/ogg',
224-
_ => 'audio/mpeg',
225-
};
227+
class _AudioFormat {
228+
const _AudioFormat(this.extension, this.mimeType);
229+
230+
final String extension;
231+
final String mimeType;
226232
}

0 commit comments

Comments
 (0)