react-native-compressor 1.8.15 → 1.8.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -9
- package/android/src/main/java/com/reactnativecompressor/Audio/AudioCompressor.kt +58 -27
- package/android/src/main/java/com/reactnativecompressor/Audio/AudioHelper.kt +49 -0
- package/android/src/main/java/com/reactnativecompressor/Audio/AudioMain.kt +2 -6
- package/android/src/main/java/com/reactnativecompressor/Utils/createVideoThumbnail.kt +0 -2
- package/android/src/main/java/com/reactnativecompressor/Video/VideoMain.kt +6 -6
- package/ios/Audio/AudioHelper.swift +49 -0
- package/ios/Audio/AudioMain.swift +46 -82
- package/ios/Audio/AudioOptions.swift +37 -0
- package/ios/Audio/FormatConverter/FormatConverter+Compressed.swift +326 -0
- package/ios/Audio/FormatConverter/FormatConverter+PCM.swift +239 -0
- package/ios/Audio/FormatConverter/FormatConverter+Utilities.swift +103 -0
- package/ios/Audio/FormatConverter/FormatConverter.swift +264 -0
- package/ios/Compressor.xcodeproj/xcuserdata/apple.xcuserdatad/xcschemes/xcschememanagement.plist +14 -0
- package/ios/Video/NextLevelSessionExporter.swift +694 -0
- package/ios/Video/VideoMain.swift +9 -8
- package/lib/commonjs/Audio/index.js +1 -3
- package/lib/commonjs/Audio/index.js.map +1 -1
- package/lib/commonjs/utils/index.js +0 -2
- package/lib/commonjs/utils/index.js.map +1 -1
- package/lib/module/Audio/index.js +1 -3
- package/lib/module/Audio/index.js.map +1 -1
- package/lib/module/utils/index.js +0 -3
- package/lib/module/utils/index.js.map +1 -1
- package/lib/typescript/Audio/index.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +7 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/utils/index.d.ts +11 -2
- package/lib/typescript/utils/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/react-native-compressor.podspec +0 -1
- package/src/Audio/index.tsx +1 -3
- package/src/utils/index.tsx +12 -3
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
// Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/
|
|
2
|
+
|
|
3
|
+
import AVFoundation
|
|
4
|
+
|
|
5
|
+
// MARK: - internal helper functions
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
public extension AVURLAsset {
|
|
9
|
+
/// Audio format for the file in the URL asset
|
|
10
|
+
var audioFormat: AVAudioFormat? {
|
|
11
|
+
// pull the input format out of the audio file...
|
|
12
|
+
if let source = try? AVAudioFile(forReading: url) {
|
|
13
|
+
return source.fileFormat
|
|
14
|
+
|
|
15
|
+
// if that fails it might be a video, so check the tracks for audio
|
|
16
|
+
} else {
|
|
17
|
+
let audioTracks = tracks.filter { $0.mediaType == .audio }
|
|
18
|
+
|
|
19
|
+
guard !audioTracks.isEmpty else { return nil }
|
|
20
|
+
|
|
21
|
+
let formatDescriptions = audioTracks.compactMap {
|
|
22
|
+
$0.formatDescriptions as? [CMFormatDescription]
|
|
23
|
+
}.reduce([], +)
|
|
24
|
+
|
|
25
|
+
let audioFormats: [AVAudioFormat] = formatDescriptions.compactMap {
|
|
26
|
+
AVAudioFormat(cmAudioFormatDescription: $0)
|
|
27
|
+
}
|
|
28
|
+
return audioFormats.first
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
extension FormatConverter {
|
|
35
|
+
/// Example of the most simplistic AVFoundation conversion.
|
|
36
|
+
/// With this approach you can't really specify any settings other than the limited presets.
|
|
37
|
+
/// No sample rate conversion in this. This isn't used in the public methods but is here
|
|
38
|
+
/// for example.
|
|
39
|
+
///
|
|
40
|
+
/// see `AVAssetExportSession`:
|
|
41
|
+
/// *Prior to initializing an instance of AVAssetExportSession, you can invoke
|
|
42
|
+
/// +allExportPresets to obtain the complete list of presets available. Use
|
|
43
|
+
/// +exportPresetsCompatibleWithAsset: to obtain a list of presets that are compatible
|
|
44
|
+
/// with a specific AVAsset.*
|
|
45
|
+
///
|
|
46
|
+
/// This is no longer used in this class as it's not possible to convert sample rate or other
|
|
47
|
+
/// required options. It will use the next function instead
|
|
48
|
+
func convertCompressed(presetName: String, completionHandler: FormatConverterCallback? = nil) {
|
|
49
|
+
guard let inputURL = inputURL else {
|
|
50
|
+
completionHandler?(Self.createError(message: "Input file can't be nil."))
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
guard let outputURL = outputURL else {
|
|
54
|
+
completionHandler?(Self.createError(message: "Output file can't be nil."))
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let asset = AVURLAsset(url: inputURL)
|
|
59
|
+
guard let session = AVAssetExportSession(asset: asset,
|
|
60
|
+
presetName: presetName) else { return }
|
|
61
|
+
|
|
62
|
+
session.determineCompatibleFileTypes { list in
|
|
63
|
+
|
|
64
|
+
guard let outputFileType: AVFileType = list.first else {
|
|
65
|
+
let error = Self.createError(message: "Unable to determine a compatible file type from \(inputURL.path)")
|
|
66
|
+
completionHandler?(error)
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// session.progress could be sent out via a delegate for this session
|
|
71
|
+
session.outputURL = outputURL
|
|
72
|
+
session.outputFileType = outputFileType
|
|
73
|
+
session.exportAsynchronously {
|
|
74
|
+
completionHandler?(session.error)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// Convert to compressed first creating a tmp file to PCM to allow more flexible conversion
|
|
80
|
+
/// options to work.
|
|
81
|
+
func convertCompressed(completionHandler: FormatConverterCallback? = nil) {
|
|
82
|
+
guard let inputURL = inputURL else {
|
|
83
|
+
completionHandler?(Self.createError(message: "Input file can't be nil."))
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
guard let outputURL = outputURL else {
|
|
87
|
+
completionHandler?(Self.createError(message: "Output file can't be nil."))
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
guard let options = options else {
|
|
92
|
+
completionHandler?(Self.createError(message: "Options can't be nil."))
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let tempName = outputURL.deletingPathExtension().lastPathComponent + "_TEMP.wav"
|
|
97
|
+
let tempFile = outputURL.deletingLastPathComponent().appendingPathComponent(tempName)
|
|
98
|
+
|
|
99
|
+
var tempOptions = FormatConverter.Options()
|
|
100
|
+
tempOptions.bitDepthRule = .lessThanOrEqual
|
|
101
|
+
tempOptions.bitDepth = 24
|
|
102
|
+
tempOptions.sampleRate = options.sampleRate
|
|
103
|
+
tempOptions.channels = options.channels
|
|
104
|
+
tempOptions.format = .wav
|
|
105
|
+
|
|
106
|
+
let tempConverter = FormatConverter(inputURL: inputURL,
|
|
107
|
+
outputURL: tempFile,
|
|
108
|
+
options: tempOptions)
|
|
109
|
+
|
|
110
|
+
tempConverter.start { error in
|
|
111
|
+
if let error = error {
|
|
112
|
+
completionHandler?(Self.createError(message: "Failed to convert input to PCM: \(error.localizedDescription)"))
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
self.inputURL = tempFile
|
|
117
|
+
|
|
118
|
+
self.convertPCMToCompressed { error in
|
|
119
|
+
try? FileManager.default.removeItem(at: tempFile)
|
|
120
|
+
completionHandler?(error)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// The AVFoundation way. *This doesn't currently handle compressed input - only compressed output.*
|
|
126
|
+
func convertPCMToCompressed(completionHandler: FormatConverterCallback? = nil) {
|
|
127
|
+
guard let inputURL = inputURL else {
|
|
128
|
+
completionHandler?(Self.createError(message: "Input file can't be nil."))
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
guard let outputURL = outputURL else {
|
|
132
|
+
completionHandler?(Self.createError(message: "Output file can't be nil."))
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
guard let options = options, let outputFormat = options.format else {
|
|
137
|
+
completionHandler?(Self.createError(message: "Options can't be nil."))
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// verify outputFormat
|
|
142
|
+
guard FormatConverter.outputFormats.contains(outputFormat) else {
|
|
143
|
+
completionHandler?(Self.createError(message: "The output file format isn't able to be produced by this class."))
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let asset = AVURLAsset(url: inputURL)
|
|
148
|
+
do {
|
|
149
|
+
self.reader = try AVAssetReader(asset: asset)
|
|
150
|
+
|
|
151
|
+
} catch let err as NSError {
|
|
152
|
+
completionHandler?(err)
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
guard let reader = reader else {
|
|
157
|
+
completionHandler?(Self.createError(message: "Unable to setup the AVAssetReader."))
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
guard let inputFormat = asset.audioFormat else {
|
|
162
|
+
completionHandler?(Self.createError(message: "Unable to read the input file format."))
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
var format: AVFileType
|
|
167
|
+
var formatKey: AudioFormatID
|
|
168
|
+
|
|
169
|
+
switch outputFormat {
|
|
170
|
+
case .m4a, .mp4:
|
|
171
|
+
format = .m4a
|
|
172
|
+
formatKey = kAudioFormatMPEG4AAC
|
|
173
|
+
case .aif:
|
|
174
|
+
format = .aiff
|
|
175
|
+
formatKey = kAudioFormatLinearPCM
|
|
176
|
+
case .caf:
|
|
177
|
+
format = .caf
|
|
178
|
+
formatKey = kAudioFormatLinearPCM
|
|
179
|
+
case .wav:
|
|
180
|
+
format = .wav
|
|
181
|
+
formatKey = kAudioFormatLinearPCM
|
|
182
|
+
default:
|
|
183
|
+
print("Unsupported output format: \(outputFormat)")
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
do {
|
|
188
|
+
self.writer = try AVAssetWriter(outputURL: outputURL, fileType: format)
|
|
189
|
+
} catch let err as NSError {
|
|
190
|
+
completionHandler?(err)
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
guard let writer = writer else {
|
|
195
|
+
completionHandler?(Self.createError(message: "Unable to setup the AVAssetWriter."))
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 1. chosen option. 2. same as input file. 3. 16 bit
|
|
200
|
+
// optional in case of compressed audio. That said, the other conversion methods are actually used in
|
|
201
|
+
// that case
|
|
202
|
+
let bitDepth = (options.bitDepth ?? inputFormat.settings[AVLinearPCMBitDepthKey] ?? 16) as Any
|
|
203
|
+
var isFloat = false
|
|
204
|
+
if let intDepth = bitDepth as? Int {
|
|
205
|
+
isFloat = intDepth == 32
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
var sampleRate = options.sampleRate ?? inputFormat.sampleRate
|
|
209
|
+
let channels = options.channels ?? inputFormat.channelCount
|
|
210
|
+
|
|
211
|
+
if sampleRate == 0 {
|
|
212
|
+
print("Sample rate can't be 0 - assigning to default format of 48k. inputFormat is", inputFormat)
|
|
213
|
+
sampleRate = 48000
|
|
214
|
+
}
|
|
215
|
+
var outputSettings: [String: Any]?
|
|
216
|
+
|
|
217
|
+
// Note: AVAssetReaderOutput does not currently support compressed audio
|
|
218
|
+
if formatKey == kAudioFormatMPEG4AAC {
|
|
219
|
+
if sampleRate > 48000 {
|
|
220
|
+
sampleRate = 48000
|
|
221
|
+
}
|
|
222
|
+
// mono should be 1/2 the shown bitrate
|
|
223
|
+
let perChannel = channels == 1 ? 2 : 1
|
|
224
|
+
|
|
225
|
+
// reset these for m4a:
|
|
226
|
+
outputSettings = [
|
|
227
|
+
AVFormatIDKey: formatKey,
|
|
228
|
+
AVSampleRateKey: sampleRate,
|
|
229
|
+
AVNumberOfChannelsKey: channels,
|
|
230
|
+
AVEncoderBitRateKey: Int(options.bitRate) / perChannel,
|
|
231
|
+
AVEncoderBitRateStrategyKey: AVAudioBitRateStrategy_Constant,
|
|
232
|
+
]
|
|
233
|
+
} else {
|
|
234
|
+
outputSettings = [
|
|
235
|
+
AVFormatIDKey: formatKey,
|
|
236
|
+
AVSampleRateKey: sampleRate,
|
|
237
|
+
AVNumberOfChannelsKey: channels,
|
|
238
|
+
AVLinearPCMBitDepthKey: bitDepth,
|
|
239
|
+
AVLinearPCMIsFloatKey: isFloat,
|
|
240
|
+
AVLinearPCMIsBigEndianKey: format != .wav,
|
|
241
|
+
AVLinearPCMIsNonInterleaved: !(options.isInterleaved ?? inputFormat.isInterleaved),
|
|
242
|
+
]
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let hint = asset.audioFormat?.formatDescription
|
|
246
|
+
|
|
247
|
+
let writerInput = AVAssetWriterInput(mediaType: .audio, outputSettings: outputSettings, sourceFormatHint: hint)
|
|
248
|
+
writer.add(writerInput)
|
|
249
|
+
|
|
250
|
+
guard let track = asset.tracks(withMediaType: .audio).first else {
|
|
251
|
+
completionProxy(error: Self.createError(message: "No audio was found in the input file."),
|
|
252
|
+
completionHandler: completionHandler)
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
let readerOutput = AVAssetReaderTrackOutput(track: track, outputSettings: nil)
|
|
257
|
+
guard reader.canAdd(readerOutput) else {
|
|
258
|
+
completionProxy(error: Self.createError(message: "Unable to add reader output."),
|
|
259
|
+
completionHandler: completionHandler)
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
reader.add(readerOutput)
|
|
263
|
+
|
|
264
|
+
if !writer.startWriting() {
|
|
265
|
+
print("Failed to start writing. Error:", writer.error?.localizedDescription)
|
|
266
|
+
completionProxy(error: writer.error,
|
|
267
|
+
completionHandler: completionHandler)
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
writer.startSession(atSourceTime: CMTime.zero)
|
|
272
|
+
|
|
273
|
+
if !reader.startReading() {
|
|
274
|
+
print("Failed to start reading. Error:", reader.error?.localizedDescription)
|
|
275
|
+
completionProxy(error: reader.error,
|
|
276
|
+
completionHandler: completionHandler)
|
|
277
|
+
return
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
let queue = DispatchQueue(label: "com.audiodesigndesk.ADD.FormatConverter.convertAsset")
|
|
281
|
+
|
|
282
|
+
// session.progress could be sent out via a delegate for this session
|
|
283
|
+
writerInput.requestMediaDataWhenReady(on: queue, using: {
|
|
284
|
+
var processing = true // safety flag to prevent runaway loops if errors
|
|
285
|
+
|
|
286
|
+
while writerInput.isReadyForMoreMediaData, processing {
|
|
287
|
+
if reader.status == .reading,
|
|
288
|
+
let buffer = readerOutput.copyNextSampleBuffer()
|
|
289
|
+
{
|
|
290
|
+
writerInput.append(buffer)
|
|
291
|
+
|
|
292
|
+
} else {
|
|
293
|
+
writerInput.markAsFinished()
|
|
294
|
+
|
|
295
|
+
switch reader.status {
|
|
296
|
+
case .failed:
|
|
297
|
+
print("Conversion failed with error", reader.error)
|
|
298
|
+
writer.cancelWriting()
|
|
299
|
+
self.completionProxy(error: reader.error, completionHandler: completionHandler)
|
|
300
|
+
case .cancelled:
|
|
301
|
+
print("Conversion cancelled")
|
|
302
|
+
self.completionProxy(error: Self.createError(message: "Process canceled"),
|
|
303
|
+
completionHandler: completionHandler)
|
|
304
|
+
case .completed:
|
|
305
|
+
writer.finishWriting {
|
|
306
|
+
switch writer.status {
|
|
307
|
+
case .failed:
|
|
308
|
+
print("Conversion failed at finishWriting")
|
|
309
|
+
self.completionProxy(error: writer.error,
|
|
310
|
+
completionHandler: completionHandler)
|
|
311
|
+
default:
|
|
312
|
+
// no errors
|
|
313
|
+
completionHandler?(nil)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
default:
|
|
317
|
+
break
|
|
318
|
+
}
|
|
319
|
+
processing = false
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}) // requestMediaDataWhenReady
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/
|
|
2
|
+
|
|
3
|
+
import AVFoundation
|
|
4
|
+
|
|
5
|
+
// MARK: - internal helper functions
|
|
6
|
+
|
|
7
|
+
extension FormatConverter {
|
|
8
|
+
func convertToPCM(completionHandler: FormatConverterCallback? = nil) {
|
|
9
|
+
guard let inputURL = inputURL else {
|
|
10
|
+
completionHandler?(Self.createError(message: "Input file can't be nil."))
|
|
11
|
+
return
|
|
12
|
+
}
|
|
13
|
+
guard let outputURL = outputURL else {
|
|
14
|
+
completionHandler?(Self.createError(message: "Output file can't be nil."))
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
guard let options = options, let outputFormat = options.format else {
|
|
19
|
+
completionHandler?(Self.createError(message: "Options can't be nil."))
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
var format: AudioFileTypeID
|
|
23
|
+
|
|
24
|
+
switch outputFormat {
|
|
25
|
+
case .aif:
|
|
26
|
+
format = kAudioFileAIFFType
|
|
27
|
+
case .wav:
|
|
28
|
+
format = kAudioFileWAVEType
|
|
29
|
+
case .caf:
|
|
30
|
+
format = kAudioFileCAFType
|
|
31
|
+
default:
|
|
32
|
+
completionHandler?(Self.createError(message: "Output file must be caf, wav or aif."))
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
var inputFile: ExtAudioFileRef?
|
|
37
|
+
var outputFile: ExtAudioFileRef?
|
|
38
|
+
|
|
39
|
+
func closeFiles() {
|
|
40
|
+
if let strongFile = inputFile {
|
|
41
|
+
if noErr != ExtAudioFileDispose(strongFile) {
|
|
42
|
+
print("Error disposing input file, could have a memory leak")
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
inputFile = nil
|
|
46
|
+
|
|
47
|
+
if let strongFile = outputFile {
|
|
48
|
+
if noErr != ExtAudioFileDispose(strongFile) {
|
|
49
|
+
print("Error disposing output file, could have a memory leak")
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
outputFile = nil
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// make sure these are closed on any exit to avoid leaking the file objects
|
|
56
|
+
defer {
|
|
57
|
+
closeFiles()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if noErr != ExtAudioFileOpenURL(inputURL as CFURL, &inputFile) {
|
|
61
|
+
completionHandler?(Self.createError(message: "Unable to open the input file."))
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
guard let strongInputFile = inputFile else {
|
|
66
|
+
completionHandler?(Self.createError(message: "Unable to open the input file."))
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
var inputDescription = AudioStreamBasicDescription()
|
|
71
|
+
var inputDescriptionSize = UInt32(MemoryLayout.stride(ofValue: inputDescription))
|
|
72
|
+
|
|
73
|
+
if noErr != ExtAudioFileGetProperty(strongInputFile,
|
|
74
|
+
kExtAudioFileProperty_FileDataFormat,
|
|
75
|
+
&inputDescriptionSize,
|
|
76
|
+
&inputDescription)
|
|
77
|
+
{
|
|
78
|
+
completionHandler?(Self.createError(message: "Unable to get the input file data format."))
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
var outputDescription = createOutputDescription(options: options,
|
|
83
|
+
outputFormatID: format,
|
|
84
|
+
inputDescription: inputDescription)
|
|
85
|
+
|
|
86
|
+
let inputFormat = AudioFileFormat(rawValue: inputURL.pathExtension.lowercased()) ?? .unknown
|
|
87
|
+
|
|
88
|
+
guard inputFormat != outputFormat ||
|
|
89
|
+
outputDescription.mSampleRate != inputDescription.mSampleRate ||
|
|
90
|
+
outputDescription.mChannelsPerFrame != inputDescription.mChannelsPerFrame ||
|
|
91
|
+
outputDescription.mBitsPerChannel != inputDescription.mBitsPerChannel
|
|
92
|
+
else {
|
|
93
|
+
print("No conversion is needed, formats are the same. Copying to", outputURL)
|
|
94
|
+
// just copy it?
|
|
95
|
+
do {
|
|
96
|
+
try FileManager.default.copyItem(at: inputURL, to: outputURL)
|
|
97
|
+
completionHandler?(nil)
|
|
98
|
+
} catch let err as NSError {
|
|
99
|
+
print(err)
|
|
100
|
+
}
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Create destination file
|
|
105
|
+
if noErr != ExtAudioFileCreateWithURL(outputURL as CFURL,
|
|
106
|
+
format,
|
|
107
|
+
&outputDescription,
|
|
108
|
+
nil,
|
|
109
|
+
AudioFileFlags.eraseFile.rawValue, // overwrite old file if present
|
|
110
|
+
&outputFile)
|
|
111
|
+
{
|
|
112
|
+
completionProxy(error: Self.createError(message: "Unable to create output file at \(outputURL.path). " +
|
|
113
|
+
"dstFormat \(outputDescription)"),
|
|
114
|
+
completionHandler: completionHandler)
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
guard let strongOutputFile = outputFile else {
|
|
119
|
+
completionProxy(error: Self.createError(message: "Output file is nil."),
|
|
120
|
+
completionHandler: completionHandler)
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// The format must be linear PCM (kAudioFormatLinearPCM).
|
|
125
|
+
// You must set this in order to encode or decode a non-PCM file data format.
|
|
126
|
+
// You may set this on PCM files to specify the data format used in your calls
|
|
127
|
+
// to read/write.
|
|
128
|
+
if noErr != ExtAudioFileSetProperty(strongInputFile,
|
|
129
|
+
kExtAudioFileProperty_ClientDataFormat,
|
|
130
|
+
inputDescriptionSize,
|
|
131
|
+
&outputDescription)
|
|
132
|
+
{
|
|
133
|
+
completionProxy(error: Self.createError(message: "Unable to set data format on input file."),
|
|
134
|
+
completionHandler: completionHandler)
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if noErr != ExtAudioFileSetProperty(strongOutputFile,
|
|
139
|
+
kExtAudioFileProperty_ClientDataFormat,
|
|
140
|
+
inputDescriptionSize,
|
|
141
|
+
&outputDescription)
|
|
142
|
+
{
|
|
143
|
+
completionProxy(error: Self.createError(message: "Unable to set the output file data format."),
|
|
144
|
+
completionHandler: completionHandler)
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
let bufferByteSize: UInt32 = 32768
|
|
148
|
+
var srcBuffer = [UInt8](repeating: 0, count: Int(bufferByteSize))
|
|
149
|
+
var sourceFrameOffset: UInt32 = 0
|
|
150
|
+
|
|
151
|
+
srcBuffer.withUnsafeMutableBytes { body in
|
|
152
|
+
while true {
|
|
153
|
+
let mBuffer = AudioBuffer(mNumberChannels: inputDescription.mChannelsPerFrame,
|
|
154
|
+
mDataByteSize: bufferByteSize,
|
|
155
|
+
mData: body.baseAddress)
|
|
156
|
+
|
|
157
|
+
var fillBufList = AudioBufferList(mNumberBuffers: 1,
|
|
158
|
+
mBuffers: mBuffer)
|
|
159
|
+
var frameCount: UInt32 = 0
|
|
160
|
+
|
|
161
|
+
if outputDescription.mBytesPerFrame > 0 {
|
|
162
|
+
frameCount = bufferByteSize / outputDescription.mBytesPerFrame
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if noErr != ExtAudioFileRead(strongInputFile,
|
|
166
|
+
&frameCount,
|
|
167
|
+
&fillBufList)
|
|
168
|
+
{
|
|
169
|
+
completionProxy(error: Self.createError(message: "Error reading from the input file."),
|
|
170
|
+
completionHandler: completionHandler)
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
// EOF
|
|
174
|
+
if frameCount == 0 { break }
|
|
175
|
+
|
|
176
|
+
sourceFrameOffset += frameCount
|
|
177
|
+
|
|
178
|
+
if noErr != ExtAudioFileWrite(strongOutputFile,
|
|
179
|
+
frameCount,
|
|
180
|
+
&fillBufList)
|
|
181
|
+
{
|
|
182
|
+
completionProxy(error: Self.createError(message: "Error reading from the output file."),
|
|
183
|
+
completionHandler: completionHandler)
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
closeFiles()
|
|
190
|
+
|
|
191
|
+
// no errors
|
|
192
|
+
completionHandler?(nil)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
func createOutputDescription(options: Options,
|
|
196
|
+
outputFormatID: AudioFormatID,
|
|
197
|
+
inputDescription: AudioStreamBasicDescription) -> AudioStreamBasicDescription
|
|
198
|
+
{
|
|
199
|
+
let mFormatID: AudioFormatID = kAudioFormatLinearPCM
|
|
200
|
+
|
|
201
|
+
let mSampleRate = options.sampleRate ?? inputDescription.mSampleRate
|
|
202
|
+
let mChannelsPerFrame = options.channels ?? inputDescription.mChannelsPerFrame
|
|
203
|
+
var mBitsPerChannel = options.bitDepth ?? inputDescription.mBitsPerChannel
|
|
204
|
+
|
|
205
|
+
// For example: don't allow upsampling to 24bit if the src is 16
|
|
206
|
+
if options.bitDepthRule == .lessThanOrEqual, mBitsPerChannel > inputDescription.mBitsPerChannel {
|
|
207
|
+
mBitsPerChannel = inputDescription.mBitsPerChannel
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
var mBytesPerFrame = mBitsPerChannel * mChannelsPerFrame / 8
|
|
211
|
+
var mBytesPerPacket = options.bitDepth == nil ? inputDescription.mBytesPerPacket : mBytesPerFrame
|
|
212
|
+
|
|
213
|
+
if mBitsPerChannel == 0 {
|
|
214
|
+
mBitsPerChannel = 16
|
|
215
|
+
mBytesPerPacket = 2 * mChannelsPerFrame
|
|
216
|
+
mBytesPerFrame = 2 * mChannelsPerFrame
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
var mFormatFlags: AudioFormatFlags = kLinearPCMFormatFlagIsPacked | kAudioFormatFlagIsSignedInteger
|
|
220
|
+
if outputFormatID == kAudioFileAIFFType {
|
|
221
|
+
mFormatFlags = mFormatFlags | kLinearPCMFormatFlagIsBigEndian
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if outputFormatID == kAudioFileWAVEType, mBitsPerChannel == 8 {
|
|
225
|
+
// if is 8 BIT PER CHANNEL, remove kAudioFormatFlagIsSignedInteger
|
|
226
|
+
mFormatFlags &= ~kAudioFormatFlagIsSignedInteger
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return AudioStreamBasicDescription(mSampleRate: mSampleRate,
|
|
230
|
+
mFormatID: mFormatID,
|
|
231
|
+
mFormatFlags: mFormatFlags,
|
|
232
|
+
mBytesPerPacket: mBytesPerPacket,
|
|
233
|
+
mFramesPerPacket: 1,
|
|
234
|
+
mBytesPerFrame: mBytesPerFrame,
|
|
235
|
+
mChannelsPerFrame: mChannelsPerFrame,
|
|
236
|
+
mBitsPerChannel: mBitsPerChannel,
|
|
237
|
+
mReserved: 0)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/
|
|
2
|
+
|
|
3
|
+
import AVFoundation
|
|
4
|
+
|
|
5
|
+
extension FormatConverter {
|
|
6
|
+
class func createError(message: String, code: Int = 1) -> NSError {
|
|
7
|
+
let userInfo = [NSLocalizedDescriptionKey: message]
|
|
8
|
+
return NSError(domain: "com.audiodesigndesk.FormatConverter.error",
|
|
9
|
+
code: code,
|
|
10
|
+
userInfo: userInfo)
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public extension FormatConverter {
|
|
15
|
+
/// Is this file a PCM file?
|
|
16
|
+
/// - Parameters:
|
|
17
|
+
/// - url: The URL to parse
|
|
18
|
+
/// - ignorePathExtension: Do a deep parse rather than rely on the path extension
|
|
19
|
+
/// - Returns: Bool or nil if it couldn't be determined
|
|
20
|
+
class func isPCM(url: URL, ignorePathExtension _: Bool = false) -> Bool? {
|
|
21
|
+
guard let value = isCompressed(url: url) else { return nil }
|
|
22
|
+
return !value
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// Compressed format or not
|
|
26
|
+
class func isCompressed(url: URL, ignorePathExtension: Bool = false) -> Bool? {
|
|
27
|
+
guard !ignorePathExtension else {
|
|
28
|
+
return isCompressedExt(url: url)
|
|
29
|
+
}
|
|
30
|
+
let ext = url.pathExtension.lowercased()
|
|
31
|
+
|
|
32
|
+
switch ext {
|
|
33
|
+
case "wav", "bwf", "aif", "aiff", "caf":
|
|
34
|
+
return false
|
|
35
|
+
|
|
36
|
+
case "m4a", "mp3", "mp4", "m4v", "mpg", "flac", "ogg":
|
|
37
|
+
return true
|
|
38
|
+
|
|
39
|
+
default:
|
|
40
|
+
// if the file extension is missing or unknown, open the file and check it
|
|
41
|
+
return isCompressedExt(url: url) ?? false
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private class func isCompressedExt(url: URL) -> Bool? {
|
|
46
|
+
var inputFile: ExtAudioFileRef?
|
|
47
|
+
|
|
48
|
+
func closeFiles() {
|
|
49
|
+
if let strongFile = inputFile {
|
|
50
|
+
// print("🗑 Disposing input", inputURL.path)
|
|
51
|
+
if noErr != ExtAudioFileDispose(strongFile) {
|
|
52
|
+
print("Error disposing input file, could have a memory leak")
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
inputFile = nil
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// make sure these are closed on any exit
|
|
59
|
+
defer {
|
|
60
|
+
closeFiles()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if noErr != ExtAudioFileOpenURL(url as CFURL,
|
|
64
|
+
&inputFile)
|
|
65
|
+
{
|
|
66
|
+
print("Unable to open", url.lastPathComponent)
|
|
67
|
+
return nil
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
guard let strongInputFile = inputFile else {
|
|
71
|
+
return nil
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
var inputDescription = AudioStreamBasicDescription()
|
|
75
|
+
var inputDescriptionSize = UInt32(MemoryLayout.stride(ofValue: inputDescription))
|
|
76
|
+
|
|
77
|
+
if noErr != ExtAudioFileGetProperty(strongInputFile,
|
|
78
|
+
kExtAudioFileProperty_FileDataFormat,
|
|
79
|
+
&inputDescriptionSize,
|
|
80
|
+
&inputDescription) {}
|
|
81
|
+
|
|
82
|
+
let mFormatID = inputDescription.mFormatID
|
|
83
|
+
|
|
84
|
+
switch mFormatID {
|
|
85
|
+
case kAudioFormatLinearPCM,
|
|
86
|
+
kAudioFormatAppleLossless: return false
|
|
87
|
+
default:
|
|
88
|
+
// basically all other format IDs are compressed
|
|
89
|
+
return true
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
extension Comparable {
|
|
95
|
+
// ie: 5.clamped(to: 7...10)
|
|
96
|
+
// ie: 5.0.clamped(to: 7.0...10.0)
|
|
97
|
+
// ie: "a".clamped(to: "b"..."h")
|
|
98
|
+
/// **OTCore:**
|
|
99
|
+
/// Returns the value clamped to the passed range.
|
|
100
|
+
@inlinable func clamped(to limits: ClosedRange<Self>) -> Self {
|
|
101
|
+
min(max(self, limits.lowerBound), limits.upperBound)
|
|
102
|
+
}
|
|
103
|
+
}
|