0de24122ad
This was motivated by the fact that recording was crashing on the native M1 build. That ended up being mostly a PEBKAC problem - turns out the Mac Mini has no built-in microphone 🤦. I still thinks this has some value though - it doesn't crash in such cases, and probably doesn't suffer from the problem shown in this thread either: https://forums.ankiweb.net/t/anki-crashes-when-trying-to-record-on-mac/14764 For now, this is only enabled when running on arm64. If it turns out to be reliable, it could be offered as an option on amd64 as well.
118 lines
3.2 KiB
Swift
118 lines
3.2 KiB
Swift
// Copyright: Ankitects Pty Ltd and contributors
|
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
import Foundation
|
|
import AVKit
|
|
|
|
enum RecordError: Error {
|
|
case noPermission
|
|
case audioFormat
|
|
case recordInvoke
|
|
case stoppedWithFailure
|
|
case encodingFailure
|
|
}
|
|
|
|
@_cdecl("start_wav_record")
|
|
public func startWavRecord(
|
|
path: UnsafePointer<CChar>,
|
|
onError: @escaping @convention(c) (UnsafePointer<CChar>) -> Void
|
|
) {
|
|
let url = URL(fileURLWithPath: String(cString: path))
|
|
AudioRecorder.shared.beginRecording(url: url, onError: { error in
|
|
error.localizedDescription.withCString { cString in
|
|
onError(cString)
|
|
}
|
|
})
|
|
}
|
|
|
|
@_cdecl("end_wav_record")
|
|
public func endWavRecord() {
|
|
AudioRecorder.shared.endRecording()
|
|
}
|
|
|
|
|
|
private class AudioRecorder: NSObject, AVAudioRecorderDelegate {
|
|
static let shared = AudioRecorder()
|
|
|
|
private var audioRecorder: AVAudioRecorder?
|
|
private var onError: ((RecordError) -> Void)?
|
|
|
|
func beginRecording(url: URL, onError: @escaping (Error) -> Void) {
|
|
self.endRecording()
|
|
|
|
requestPermission { success in
|
|
if !success {
|
|
onError(RecordError.noPermission)
|
|
return
|
|
}
|
|
|
|
do {
|
|
try self.beginRecordingInner(url: url)
|
|
} catch {
|
|
onError(error)
|
|
return
|
|
}
|
|
self.onError = onError
|
|
}
|
|
|
|
}
|
|
|
|
func endRecording() {
|
|
if let recorder = audioRecorder {
|
|
recorder.stop()
|
|
}
|
|
audioRecorder = nil
|
|
onError = nil
|
|
}
|
|
|
|
/// Request permission, then call provided callback (true on success).
|
|
private func requestPermission(completionHandler: @escaping (Bool) -> Void) {
|
|
switch AVCaptureDevice.authorizationStatus(for: .audio) {
|
|
case .notDetermined:
|
|
AVCaptureDevice.requestAccess(
|
|
for: .audio,
|
|
completionHandler: completionHandler
|
|
)
|
|
return
|
|
case .authorized:
|
|
completionHandler(true)
|
|
return
|
|
case .restricted:
|
|
print("recording restricted")
|
|
case .denied:
|
|
print("recording denied")
|
|
@unknown default:
|
|
print("recording unknown permission")
|
|
}
|
|
completionHandler(false)
|
|
}
|
|
|
|
private func beginRecordingInner(url: URL) throws {
|
|
guard let audioFormat = AVAudioFormat.init(
|
|
commonFormat: .pcmFormatInt16,
|
|
sampleRate: 44100,
|
|
channels: 1,
|
|
interleaved: true
|
|
) else {
|
|
throw RecordError.audioFormat
|
|
}
|
|
let recorder = try AVAudioRecorder(url: url, format: audioFormat)
|
|
if !recorder.record() {
|
|
throw RecordError.recordInvoke
|
|
}
|
|
audioRecorder = recorder
|
|
}
|
|
|
|
|
|
func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) {
|
|
if !flag {
|
|
onError?(.stoppedWithFailure)
|
|
}
|
|
}
|
|
|
|
|
|
func audioRecorderEncodeErrorDidOccur(_ recorder: AVAudioRecorder, error: Error?) {
|
|
onError?(.encodingFailure)
|
|
}
|
|
}
|