From 71fd2c0509710c1112a89bee1656ed3ce7405aab Mon Sep 17 00:00:00 2001
From: Timo Rohner
Date: Mon, 11 Mar 2019 16:54:07 +0100
Subject: [PATCH 01/19] Add CoreML Model + supporting code
---
Baby Monitor.xcodeproj/project.pbxproj | 88 ++++++++----
.../Source Files/AppDependencies.swift | 8 +-
.../Services/Crying/AudioRecordService.swift | 81 -----------
.../Crying/CryingDetectionService.swift | 48 -------
.../AudioMicrophoneService.swift | 127 ++++++++++++++++++
.../CryingDetectionService.swift | 68 ++++++++++
.../CryingEventService.swift | 21 +--
.../AudioSpectrogramLayer.swift | 97 +++++++++++++
.../Services/MachineLearning/MfccLayer.swift | 99 ++++++++++++++
.../MachineLearning/MfccMelFilterbank.swift | 110 +++++++++++++++
.../Services/MachineLearning/MfccOp.swift | 75 +++++++++++
.../MachineLearning/SpectrogramOp.swift | 124 +++++++++++++++++
.../MediaPlayer/MicrophoneFactory.swift | 91 +++++++++++++
.../Services/MediaPlayer/NodeCapture.swift | 111 +++++++++++++++
.../MediaPlayer/RecorderFactory.swift | 53 --------
PeerConnectionFactoryProtocol.swift | 4 +-
audioprocessing.mlmodel | Bin 0 -> 557 bytes
crydetection.mlmodel | Bin 0 -> 271989 bytes
18 files changed, 983 insertions(+), 222 deletions(-)
delete mode 100644 Baby Monitor/Source Files/Services/Crying/AudioRecordService.swift
delete mode 100644 Baby Monitor/Source Files/Services/Crying/CryingDetectionService.swift
create mode 100644 Baby Monitor/Source Files/Services/CryingDetection/AudioMicrophoneService.swift
create mode 100644 Baby Monitor/Source Files/Services/CryingDetection/CryingDetectionService.swift
rename Baby Monitor/Source Files/Services/{Crying => CryingDetection}/CryingEventService.swift (76%)
create mode 100644 Baby Monitor/Source Files/Services/MachineLearning/AudioSpectrogramLayer.swift
create mode 100644 Baby Monitor/Source Files/Services/MachineLearning/MfccLayer.swift
create mode 100644 Baby Monitor/Source Files/Services/MachineLearning/MfccMelFilterbank.swift
create mode 100644 Baby Monitor/Source Files/Services/MachineLearning/MfccOp.swift
create mode 100644 Baby Monitor/Source Files/Services/MachineLearning/SpectrogramOp.swift
create mode 100644 Baby Monitor/Source Files/Services/MediaPlayer/MicrophoneFactory.swift
create mode 100644 Baby Monitor/Source Files/Services/MediaPlayer/NodeCapture.swift
delete mode 100644 Baby Monitor/Source Files/Services/MediaPlayer/RecorderFactory.swift
create mode 100644 audioprocessing.mlmodel
create mode 100644 crydetection.mlmodel
diff --git a/Baby Monitor.xcodeproj/project.pbxproj b/Baby Monitor.xcodeproj/project.pbxproj
index 6c9c79d..1881e70 100644
--- a/Baby Monitor.xcodeproj/project.pbxproj
+++ b/Baby Monitor.xcodeproj/project.pbxproj
@@ -142,9 +142,6 @@
8A7A60CC21A40D2F00488ED4 /* CryingDetectionServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A7A60CB21A40D2F00488ED4 /* CryingDetectionServiceTests.swift */; };
8A7A60CE21A40D3B00488ED4 /* MicrophoneTrackerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A7A60CD21A40D3B00488ED4 /* MicrophoneTrackerMock.swift */; };
8A7A60D221A416E300488ED4 /* WebRtcClientManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A7A60D121A416E300488ED4 /* WebRtcClientManagerProtocol.swift */; };
- 8A7A611521A59E3B00488ED4 /* AudioRecordService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A7A611221A59E3A00488ED4 /* AudioRecordService.swift */; };
- 8A7A611621A59E3B00488ED4 /* CryingDetectionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A7A611321A59E3B00488ED4 /* CryingDetectionService.swift */; };
- 8A7A611721A59E3B00488ED4 /* CryingEventService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A7A611421A59E3B00488ED4 /* CryingEventService.swift */; };
8A7A611B21A59E5000488ED4 /* MemoryCleaner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A7A611821A59E5000488ED4 /* MemoryCleaner.swift */; };
8A7A611C21A59E5000488ED4 /* ActivityLogEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A7A611921A59E5000488ED4 /* ActivityLogEvent.swift */; };
8A7A611D21A59E5000488ED4 /* ActivityLogEventsRepositoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A7A611A21A59E5000488ED4 /* ActivityLogEventsRepositoryProtocol.swift */; };
@@ -154,7 +151,6 @@
8A7A612521A59E7500488ED4 /* FileManager+Size.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A7A612321A59E7500488ED4 /* FileManager+Size.swift */; };
8A7A612721A59E8600488ED4 /* DirectoryDocumentsSavable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A7A612621A59E8600488ED4 /* DirectoryDocumentsSavable.swift */; };
8A7A612A21A59E9A00488ED4 /* AlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A7A612921A59E9A00488ED4 /* AlertPresenter.swift */; };
- 8A7A612C21A59EB000488ED4 /* RecorderFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A7A612B21A59EB000488ED4 /* RecorderFactory.swift */; };
8A7A612E21A5A35300488ED4 /* WebRtcServerManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A7A612D21A5A35300488ED4 /* WebRtcServerManagerProtocol.swift */; };
8A7A613E21A6AACD00488ED4 /* MessageServerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A7A613D21A6AACD00488ED4 /* MessageServerMock.swift */; };
8A7A614921A6AC7900488ED4 /* CryingDetectionServiceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A7A614421A6AC7800488ED4 /* CryingDetectionServiceMock.swift */; };
@@ -239,6 +235,18 @@
A1ED8F912212C745005762E8 /* TwoOptionsBaseOnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1ED8F902212C745005762E8 /* TwoOptionsBaseOnboardingView.swift */; };
A1ED8F932212CA35005762E8 /* OldBaseOnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1ED8F922212CA35005762E8 /* OldBaseOnboardingView.swift */; };
A1ED8F952212CB8D005762E8 /* OnboardingContinuableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1ED8F942212CB8D005762E8 /* OnboardingContinuableViewController.swift */; };
+ A7D756082232DBC800F9893E /* CryingEventService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D756052232DBC800F9893E /* CryingEventService.swift */; };
+ A7D756092232DBC800F9893E /* CryingDetectionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D756062232DBC800F9893E /* CryingDetectionService.swift */; };
+ A7D7561F2232DE8700F9893E /* MicrophoneFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D7561E2232DE8700F9893E /* MicrophoneFactory.swift */; };
+ A7D756212232DF0A00F9893E /* AudioMicrophoneService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D756202232DF0A00F9893E /* AudioMicrophoneService.swift */; };
+ A7D7562522344B0300F9893E /* NodeCapture.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D7562422344B0300F9893E /* NodeCapture.swift */; };
+ A7D756272236AFA000F9893E /* crydetection.mlmodel in Sources */ = {isa = PBXBuildFile; fileRef = A7D756262236AF9F00F9893E /* crydetection.mlmodel */; };
+ A7D756292236AFB800F9893E /* audioprocessing.mlmodel in Sources */ = {isa = PBXBuildFile; fileRef = A7D756282236AFB700F9893E /* audioprocessing.mlmodel */; };
+ A7D756302236B03D00F9893E /* MfccMelFilterbank.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D7562B2236B03D00F9893E /* MfccMelFilterbank.swift */; };
+ A7D756312236B03D00F9893E /* MfccLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D7562C2236B03D00F9893E /* MfccLayer.swift */; };
+ A7D756322236B03D00F9893E /* AudioSpectrogramLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D7562D2236B03D00F9893E /* AudioSpectrogramLayer.swift */; };
+ A7D756332236B03D00F9893E /* MfccOp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D7562E2236B03D00F9893E /* MfccOp.swift */; };
+ A7D756342236B03D00F9893E /* SpectrogramOp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D7562F2236B03D00F9893E /* SpectrogramOp.swift */; };
EC4C9947216B946B0093EDFC /* OnboardingCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC4C9946216B946B0093EDFC /* OnboardingCoordinator.swift */; };
EC82B55A213EA072005CA395 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC82B559213EA072005CA395 /* AppDelegate.swift */; };
EC82B561213EA074005CA395 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EC82B560213EA074005CA395 /* Assets.xcassets */; };
@@ -454,9 +462,6 @@
8A7A60CB21A40D2F00488ED4 /* CryingDetectionServiceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CryingDetectionServiceTests.swift; path = "Baby MonitorTests/Services/CryingDetectionServiceTests.swift"; sourceTree = SOURCE_ROOT; };
8A7A60CD21A40D3B00488ED4 /* MicrophoneTrackerMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MicrophoneTrackerMock.swift; sourceTree = ""; };
8A7A60D121A416E300488ED4 /* WebRtcClientManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRtcClientManagerProtocol.swift; sourceTree = ""; };
- 8A7A611221A59E3A00488ED4 /* AudioRecordService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AudioRecordService.swift; path = Crying/AudioRecordService.swift; sourceTree = ""; };
- 8A7A611321A59E3B00488ED4 /* CryingDetectionService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CryingDetectionService.swift; path = Crying/CryingDetectionService.swift; sourceTree = ""; };
- 8A7A611421A59E3B00488ED4 /* CryingEventService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CryingEventService.swift; path = Crying/CryingEventService.swift; sourceTree = ""; };
8A7A611821A59E5000488ED4 /* MemoryCleaner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MemoryCleaner.swift; sourceTree = ""; };
8A7A611921A59E5000488ED4 /* ActivityLogEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityLogEvent.swift; sourceTree = ""; };
8A7A611A21A59E5000488ED4 /* ActivityLogEventsRepositoryProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityLogEventsRepositoryProtocol.swift; sourceTree = ""; };
@@ -466,7 +471,6 @@
8A7A612321A59E7500488ED4 /* FileManager+Size.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "FileManager+Size.swift"; sourceTree = ""; };
8A7A612621A59E8600488ED4 /* DirectoryDocumentsSavable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DirectoryDocumentsSavable.swift; sourceTree = ""; };
8A7A612921A59E9A00488ED4 /* AlertPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AlertPresenter.swift; path = "Baby Monitor/Source Files/Services/Other/AlertPresenter.swift"; sourceTree = SOURCE_ROOT; };
- 8A7A612B21A59EB000488ED4 /* RecorderFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecorderFactory.swift; sourceTree = ""; };
8A7A612D21A5A35300488ED4 /* WebRtcServerManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRtcServerManagerProtocol.swift; sourceTree = ""; };
8A7A613D21A6AACD00488ED4 /* MessageServerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageServerMock.swift; sourceTree = ""; };
8A7A614421A6AC7800488ED4 /* CryingDetectionServiceMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CryingDetectionServiceMock.swift; sourceTree = ""; };
@@ -550,6 +554,18 @@
A1ED8F902212C745005762E8 /* TwoOptionsBaseOnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwoOptionsBaseOnboardingView.swift; sourceTree = ""; };
A1ED8F922212CA35005762E8 /* OldBaseOnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OldBaseOnboardingView.swift; sourceTree = ""; };
A1ED8F942212CB8D005762E8 /* OnboardingContinuableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingContinuableViewController.swift; sourceTree = ""; };
+ A7D756052232DBC800F9893E /* CryingEventService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CryingEventService.swift; sourceTree = ""; };
+ A7D756062232DBC800F9893E /* CryingDetectionService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CryingDetectionService.swift; sourceTree = ""; };
+ A7D7561E2232DE8700F9893E /* MicrophoneFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MicrophoneFactory.swift; sourceTree = ""; };
+ A7D756202232DF0A00F9893E /* AudioMicrophoneService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioMicrophoneService.swift; sourceTree = ""; };
+ A7D7562422344B0300F9893E /* NodeCapture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeCapture.swift; sourceTree = ""; };
+ A7D756262236AF9F00F9893E /* crydetection.mlmodel */ = {isa = PBXFileReference; lastKnownFileType = file.mlmodel; path = crydetection.mlmodel; sourceTree = ""; };
+ A7D756282236AFB700F9893E /* audioprocessing.mlmodel */ = {isa = PBXFileReference; lastKnownFileType = file.mlmodel; path = audioprocessing.mlmodel; sourceTree = ""; };
+ A7D7562B2236B03D00F9893E /* MfccMelFilterbank.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MfccMelFilterbank.swift; sourceTree = ""; };
+ A7D7562C2236B03D00F9893E /* MfccLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MfccLayer.swift; sourceTree = ""; };
+ A7D7562D2236B03D00F9893E /* AudioSpectrogramLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioSpectrogramLayer.swift; sourceTree = ""; };
+ A7D7562E2236B03D00F9893E /* MfccOp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MfccOp.swift; sourceTree = ""; };
+ A7D7562F2236B03D00F9893E /* SpectrogramOp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpectrogramOp.swift; sourceTree = ""; };
DC0A7360EB7E7A40F5162D1F /* Pods_Baby_MonitorTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Baby_MonitorTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
EC4C9946216B946B0093EDFC /* OnboardingCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingCoordinator.swift; sourceTree = ""; };
EC82B556213EA072005CA395 /* BabyMonitor.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BabyMonitor.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -760,9 +776,10 @@
4E63DC0C21E39CFA00604167 /* Notifications */,
8A7A612821A59E9100488ED4 /* Other */,
8A7A608221A4024100488ED4 /* ErrorHandler */,
- 8A7A608121A4020B00488ED4 /* CryingDetection */,
8AFAE5A9219AAA40007013BC /* Websocket */,
8A43DAFB217DD1C300E3004D /* Connection */,
+ A7D756042232DBC800F9893E /* CryingDetection */,
+ A7D7562A2236AFD800F9893E /* MachineLearning */,
4E39432F2175D48C00AD7582 /* NetService */,
8AEAFBAF217735B3003E756F /* Persistence */,
4E5731A12170BA9D00DEAF0B /* MediaPlayer */,
@@ -773,8 +790,9 @@
4E5731A12170BA9D00DEAF0B /* MediaPlayer */ = {
isa = PBXGroup;
children = (
- 8A7A612B21A59EB000488ED4 /* RecorderFactory.swift */,
+ A7D7561E2232DE8700F9893E /* MicrophoneFactory.swift */,
8AA7CBE1218C576D00FCF62A /* URLMediaPlayer.swift */,
+ A7D7562422344B0300F9893E /* NodeCapture.swift */,
);
path = MediaPlayer;
sourceTree = "";
@@ -1145,16 +1163,6 @@
path = "New Group";
sourceTree = "";
};
- 8A7A608121A4020B00488ED4 /* CryingDetection */ = {
- isa = PBXGroup;
- children = (
- 8A7A611221A59E3A00488ED4 /* AudioRecordService.swift */,
- 8A7A611321A59E3B00488ED4 /* CryingDetectionService.swift */,
- 8A7A611421A59E3B00488ED4 /* CryingEventService.swift */,
- );
- name = CryingDetection;
- sourceTree = "";
- };
8A7A608221A4024100488ED4 /* ErrorHandler */ = {
isa = PBXGroup;
children = (
@@ -1360,6 +1368,28 @@
path = CustomAnimations;
sourceTree = "";
};
+ A7D756042232DBC800F9893E /* CryingDetection */ = {
+ isa = PBXGroup;
+ children = (
+ A7D756202232DF0A00F9893E /* AudioMicrophoneService.swift */,
+ A7D756062232DBC800F9893E /* CryingDetectionService.swift */,
+ A7D756052232DBC800F9893E /* CryingEventService.swift */,
+ );
+ path = CryingDetection;
+ sourceTree = "";
+ };
+ A7D7562A2236AFD800F9893E /* MachineLearning */ = {
+ isa = PBXGroup;
+ children = (
+ A7D7562D2236B03D00F9893E /* AudioSpectrogramLayer.swift */,
+ A7D7562C2236B03D00F9893E /* MfccLayer.swift */,
+ A7D7562B2236B03D00F9893E /* MfccMelFilterbank.swift */,
+ A7D7562E2236B03D00F9893E /* MfccOp.swift */,
+ A7D7562F2236B03D00F9893E /* SpectrogramOp.swift */,
+ );
+ path = MachineLearning;
+ sourceTree = "";
+ };
EC4C9945216B944B0093EDFC /* Onboarding */ = {
isa = PBXGroup;
children = (
@@ -1389,6 +1419,8 @@
EC82B54D213EA072005CA395 = {
isa = PBXGroup;
children = (
+ A7D756282236AFB700F9893E /* audioprocessing.mlmodel */,
+ A7D756262236AF9F00F9893E /* crydetection.mlmodel */,
ECB146D9213EA44C00C504FD /* Configuration */,
EC82B558213EA072005CA395 /* Baby Monitor */,
EC82B56D213EA074005CA395 /* Baby MonitorTests */,
@@ -1801,18 +1833,20 @@
files = (
8A6098C421EBFDA700592B01 /* PeerConnectionFactoryProtocol.swift in Sources */,
4E7B091321E6148600EDDD11 /* URLSessionProtocol.swift in Sources */,
+ A7D756092232DBC800F9893E /* CryingDetectionService.swift in Sources */,
A1ED8F912212C745005762E8 /* TwoOptionsBaseOnboardingView.swift in Sources */,
4E1D2C5F216751F400E92F29 /* CameraPreviewViewModel.swift in Sources */,
A1EC9777221AA53900C27786 /* OnboardingAccessViewModel.swift in Sources */,
4E63DC1B21E39D4B00604167 /* Request.swift in Sources */,
- 8A7A611721A59E3B00488ED4 /* CryingEventService.swift in Sources */,
8A045D5B21A3F685006A2E10 /* (null) in Sources */,
+ A7D756342236B03D00F9893E /* SpectrogramOp.swift in Sources */,
A18681912209A1A60069E521 /* SendRecordingsIntroFeatureView.swift in Sources */,
8A7A611B21A59E5000488ED4 /* MemoryCleaner.swift in Sources */,
8A7A611C21A59E5000488ED4 /* ActivityLogEvent.swift in Sources */,
4E1D2C6521678D9100E92F29 /* TypedViewController.swift in Sources */,
4EA2802621E8C6B200262E45 /* DateFormatter+Helpers.swift in Sources */,
A1640E2B22048EA100398ED2 /* BabyMonitorSwitch.swift in Sources */,
+ A7D7562522344B0300F9893E /* NodeCapture.swift in Sources */,
8AED318E219045AD00FEFE8A /* BabyMonitorCellDeletable.swift in Sources */,
A18D3E7621FF6D3700C165C6 /* ActivityLogViewController.swift in Sources */,
A1572D8B22005AF600AC57C4 /* ActivityLogCell.swift in Sources */,
@@ -1850,8 +1884,8 @@
8A50AA3A21E602F60058C63A /* SessionDescriptionProtocol.swift in Sources */,
A1ED8F8D2212C6EA005762E8 /* ContinuableBaseOnboardingView.swift in Sources */,
4E57319C2170BA7400DEAF0B /* BabyMonitorCellSelectable.swift in Sources */,
+ A7D756292236AFB800F9893E /* audioprocessing.mlmodel in Sources */,
4E57319A2170BA7400DEAF0B /* HasNavigationController.swift in Sources */,
- 8A7A611621A59E3B00488ED4 /* CryingDetectionService.swift in Sources */,
4E6EA7982161FEEF005575E0 /* RootCoordinator.swift in Sources */,
4E8E057421AE9E4A009ACE05 /* UIColor+BabyMonitor.swift in Sources */,
8A43DB02217DD4A500E3004D /* NetServiceConnectionChecker.swift in Sources */,
@@ -1871,7 +1905,6 @@
8A7A616D21A8162000488ED4 /* UIColor+Custom.swift in Sources */,
A1ED8F952212CB8D005762E8 /* OnboardingContinuableViewController.swift in Sources */,
EC8A8FF321AE8EE500B5FCCF /* BasePageViewController.swift in Sources */,
- 8A7A611521A59E3B00488ED4 /* AudioRecordService.swift in Sources */,
4EB6DE6321B6687D004701EA /* EventMessage.swift in Sources */,
8A8EF944219C40630098A27B /* RTCSessionDescription+JSON.swift in Sources */,
8AFAE5C3219C1FE3007013BC /* SdpAnswerDecoder.swift in Sources */,
@@ -1894,11 +1927,14 @@
EC8A8FF521AE8F5F00B5FCCF /* TypedPageViewController.swift in Sources */,
8A8B76DF21EDE3640063EF7E /* WebsocketMessageDecodable.swift in Sources */,
4E7A1A9221F12BD00015C3A3 /* ServerCoordinator.swift in Sources */,
+ A7D756082232DBC800F9893E /* CryingEventService.swift in Sources */,
A1640E272203A48F00398ED2 /* ServerSettingsViewModel.swift in Sources */,
ECB2428A21AD4F9200B4E409 /* IntroViewController.swift in Sources */,
4E1D2C67216791AD00E92F29 /* DashboardView.swift in Sources */,
A1492A66221D814500AA7716 /* ClearableLazyItem.swift in Sources */,
+ A7D756322236B03D00F9893E /* AudioSpectrogramLayer.swift in Sources */,
8A7A612A21A59E9A00488ED4 /* AlertPresenter.swift in Sources */,
+ A7D756212232DF0A00F9893E /* AudioMicrophoneService.swift in Sources */,
4E9563392164F09C00289475 /* BabyNavigationItemView.swift in Sources */,
4E63DC1C21E39D4B00604167 /* FirebasePushNotificationsRequest.swift in Sources */,
4EB6DE6521B67C11004701EA /* EventMessageDecoder.swift in Sources */,
@@ -1920,21 +1956,25 @@
8A7A608821A403D500488ED4 /* MicrophoneTrackerProtocol.swift in Sources */,
A1ED8F932212CA35005762E8 /* OldBaseOnboardingView.swift in Sources */,
4E1CF817217856C500F48706 /* ServerViewModel.swift in Sources */,
+ A7D756302236B03D00F9893E /* MfccMelFilterbank.swift in Sources */,
8AEAFBB62177371B003E756F /* RealmBaby.swift in Sources */,
4E2F413521B1441600513843 /* OldOnboardingContinuableViewController.swift in Sources */,
8A7A60D221A416E300488ED4 /* WebRtcClientManagerProtocol.swift in Sources */,
4E63DC1021E39CFA00604167 /* NotificationService.swift in Sources */,
8AEAFBB3217736A8003E756F /* RealmBabiesRepository.swift in Sources */,
4E7A1A9021F1284B0015C3A3 /* UserDefaults+AppMode.swift in Sources */,
+ A7D756332236B03D00F9893E /* MfccOp.swift in Sources */,
4E1D2C5D21673FF800E92F29 /* CameraPreviewViewController.swift in Sources */,
200681C121FB53AA00340596 /* SettingsCoordinator.swift in Sources */,
4E95631B21638D1500289475 /* Localizable.swift in Sources */,
+ A7D756272236AFA000F9893E /* crydetection.mlmodel in Sources */,
8A50AA4021E603080058C63A /* WebRtcStreamId.swift in Sources */,
8A7A612421A59E7500488ED4 /* FileManager+DocumentsDirectories.swift in Sources */,
8A7A612121A59E6500488ED4 /* Result.swift in Sources */,
4E57319F2170BA7400DEAF0B /* Coordinator.swift in Sources */,
8A50AA4221E60A190058C63A /* RTCPeerConnectionDelegateProxy.swift in Sources */,
8A50AA3821E602F60058C63A /* PeerConnectionProtocol.swift in Sources */,
+ A7D756312236B03D00F9893E /* MfccLayer.swift in Sources */,
4E9563362164E77100289475 /* UITableView+Dequeue.swift in Sources */,
4E36A3AE21B15F850058DAD2 /* OnboardingClientSetupViewController.swift in Sources */,
8A7A612721A59E8600488ED4 /* DirectoryDocumentsSavable.swift in Sources */,
@@ -1954,10 +1994,10 @@
8A8B76EB21EF48980063EF7E /* WebSocketWebRtcService.swift in Sources */,
8A43DAFD217DD1D000E3004D /* ConnectionChecker.swift in Sources */,
4E9563322164DDC100289475 /* Baby.swift in Sources */,
+ A7D7561F2232DE8700F9893E /* MicrophoneFactory.swift in Sources */,
4E8E057221AE7B6A009ACE05 /* SpecifyDeviceOnboardingViewModel.swift in Sources */,
8A42DB142179DCAC00BF5F1B /* URL+Prefix.swift in Sources */,
95368F552209BC6A006E263A /* SpecifyDeviceInfoOnboardingView.swift in Sources */,
- 8A7A612C21A59EB000488ED4 /* RecorderFactory.swift in Sources */,
8A8EF94D219C52480098A27B /* AnyMessageDecoder.swift in Sources */,
A1EC977D221ADC8100C27786 /* OnboardingSpinnerView.swift in Sources */,
8A43DB1F2181D1E700E3004D /* GeneralSection.swift in Sources */,
diff --git a/Baby Monitor/Source Files/AppDependencies.swift b/Baby Monitor/Source Files/AppDependencies.swift
index 3d4c1f7..6115b81 100644
--- a/Baby Monitor/Source Files/AppDependencies.swift
+++ b/Baby Monitor/Source Files/AppDependencies.swift
@@ -15,14 +15,14 @@ final class AppDependencies {
private let bag = DisposeBag()
/// Service for cleaning too many crying events
private(set) lazy var memoryCleaner: MemoryCleanerProtocol = MemoryCleaner()
- /// Service for recording audio
- private(set) lazy var audioRecordService: AudioRecordServiceProtocol? = try? AudioRecordService(recorderFactory: AudioKitRecorderFactory.makeRecorderFactory)
+ /// Service for capturing/recording microphone audio
+ private(set) lazy var audioMicrophoneService: AudioMicrophoneServiceProtocol? = try? AudioMicrophoneService(microphoneFactory: AudioKitMicrophoneFactory.makeMicrophoneFactory)
/// Service for detecting baby's cry
- private(set) lazy var cryingDetectionService: CryingDetectionServiceProtocol = CryingDetectionService(microphoneTracker: AKMicrophoneTracker())
+ private(set) lazy var cryingDetectionService: CryingDetectionServiceProtocol = CryingDetectionService(microphoneCapture: audioMicrophoneService)
/// Service that takes care of appropriate controling: crying detection, audio recording and saving these events to realm database
private(set) lazy var cryingEventService: CryingEventsServiceProtocol = CryingEventService(
cryingDetectionService: cryingDetectionService,
- audioRecordService: audioRecordService,
+ microphoneRecord: audioMicrophoneService,
activityLogEventsRepository: databaseRepository,
storageService: storageServerService)
diff --git a/Baby Monitor/Source Files/Services/Crying/AudioRecordService.swift b/Baby Monitor/Source Files/Services/Crying/AudioRecordService.swift
deleted file mode 100644
index 387f942..0000000
--- a/Baby Monitor/Source Files/Services/Crying/AudioRecordService.swift
+++ /dev/null
@@ -1,81 +0,0 @@
-//
-// CryingRecordService.swift
-// Baby Monitor
-//
-
-import Foundation
-import AudioKit
-import RxSwift
-import RxCocoa
-
-protocol ErrorProducable {
- var errorObservable: Observable { get }
-}
-
-protocol AudioRecordServiceProtocol {
- var directoryDocumentsSavableObservable: Observable { get }
- var isRecording: Bool { get }
-
- func stopRecording()
- func startRecording()
-}
-
-final class AudioRecordService: AudioRecordServiceProtocol, ErrorProducable {
-
- enum AudioError: Error {
- case initializationFailure
- case recordFailure
- case saveFailure
- }
-
- lazy var errorObservable = errorSubject.asObservable()
- lazy var directoryDocumentsSavableObservable = directoryDocumentsSavableSubject.asObservable()
-
- private(set) var isRecording = false
- private var recorder: RecorderProtocol?
- private let errorSubject = PublishSubject()
- private let directoryDocumentsSavableSubject = PublishSubject()
-
- init(recorderFactory: () -> RecorderProtocol?) throws {
- recorder = recorderFactory()
- if recorder == nil {
- throw(AudioRecordService.AudioError.initializationFailure)
- }
- }
-
- func stopRecording() {
- guard isRecording else {
- return
- }
- recorder?.stop()
- isRecording = false
- guard let audioFile = recorder?.audioFile else {
- errorSubject.onNext(AudioError.recordFailure)
- return
- }
- directoryDocumentsSavableSubject.onNext(audioFile)
- }
-
- func startRecording() {
- guard !isRecording else {
- return
- }
- do {
- try recorder?.reset()
- try recorder?.record()
- isRecording = true
- } catch {
- errorSubject.onNext(AudioError.recordFailure)
- }
- }
-}
-
-protocol RecorderProtocol: Any {
- var audioFile: AKAudioFile? { get }
-
- func stop()
- func record() throws
- func reset() throws
-}
-
-extension AKNodeRecorder: RecorderProtocol {}
diff --git a/Baby Monitor/Source Files/Services/Crying/CryingDetectionService.swift b/Baby Monitor/Source Files/Services/Crying/CryingDetectionService.swift
deleted file mode 100644
index a260b68..0000000
--- a/Baby Monitor/Source Files/Services/Crying/CryingDetectionService.swift
+++ /dev/null
@@ -1,48 +0,0 @@
-//
-// CryingDetectionService.swift
-// Baby Monitor
-//
-
-import Foundation
-import AudioKit
-import RxSwift
-import RxCocoa
-
-protocol CryingDetectionServiceProtocol: Any {
-
- /// Observable that informs about detection of baby's cry
- var cryingDetectionObservable: Observable { get }
-
- /// Starts crying detection
- func startAnalysis()
- /// Stops crying detection
- func stopAnalysis()
-}
-
-final class CryingDetectionService: CryingDetectionServiceProtocol {
-
- lazy var cryingDetectionObservable: Observable = Observable.timer(0, period: 0.2, scheduler: MainScheduler.asyncInstance)
- .map { [unowned self] _ in self.microphoneTracker.frequency }
- .filter { $0 > 1000 }
- .buffer(timeSpan: 10, count: 10, scheduler: ConcurrentDispatchQueueScheduler(queue: bufferQueue))
- .map { $0.count > 9 }
- .distinctUntilChanged()
- .subscribeOn(MainScheduler.asyncInstance)
- .share()
-
- private var isCryingEventDetected = false
- private let microphoneTracker: MicrophoneTrackerProtocol
- private let bufferQueue = DispatchQueue(label: "bufferQueue", attributes: .concurrent)
-
- init(microphoneTracker: MicrophoneTrackerProtocol) {
- self.microphoneTracker = microphoneTracker
- }
-
- func startAnalysis() {
- microphoneTracker.start()
- }
-
- func stopAnalysis() {
- microphoneTracker.stop()
- }
-}
diff --git a/Baby Monitor/Source Files/Services/CryingDetection/AudioMicrophoneService.swift b/Baby Monitor/Source Files/Services/CryingDetection/AudioMicrophoneService.swift
new file mode 100644
index 0000000..ffa077f
--- /dev/null
+++ b/Baby Monitor/Source Files/Services/CryingDetection/AudioMicrophoneService.swift
@@ -0,0 +1,127 @@
+//
+// AudioMicrophoneService.swift
+// Baby Monitor
+//
+
+
+import Foundation
+import AudioKit
+import RxSwift
+import RxCocoa
+
+protocol ErrorProducable {
+ var errorObservable: Observable { get }
+}
+
+
+protocol AudioMicrophoneRecordServiceProtocol {
+ var directoryDocumentsSavableObservable: Observable { get }
+ var isRecording: Bool { get }
+
+ func stopRecording()
+ func startRecording()
+}
+
+
+protocol AudioMicrophoneCaptureServiceProtocol {
+ var microphoneBufferReadableObservable: Observable { get }
+ var isCapturing: Bool { get }
+
+ func stopCapturing()
+ func startCapturing()
+}
+
+protocol AudioMicrophoneServiceProtocol: AudioMicrophoneRecordServiceProtocol, AudioMicrophoneCaptureServiceProtocol {}
+
+final class AudioMicrophoneService: AudioMicrophoneServiceProtocol, ErrorProducable {
+
+ enum AudioError: Error {
+ case initializationFailure
+ case captureFailure
+ case recordFailure
+ case saveFailure
+ }
+
+ lazy var errorObservable = errorSubject.asObservable()
+ lazy var microphoneBufferReadableObservable = microphoneBufferReadableSubject.asObservable()
+ lazy var directoryDocumentsSavableObservable = directoryDocumentsSavableSubject.asObservable()
+
+
+
+
+ private(set) var isCapturing = false
+ private(set) var isRecording = false
+
+ private var capture: MicrophoneCaptureProtocol
+ private var record: MicrophoneRecordProtocol
+
+ private let errorSubject = PublishSubject()
+ private let microphoneBufferReadableSubject = PublishSubject()
+ private let directoryDocumentsSavableSubject = PublishSubject()
+
+ private let disposeBag = DisposeBag()
+
+
+ init(microphoneFactory: () throws -> AudioKitMicrophoneProtocol?) throws {
+ guard let audioKitMicrophone = try microphoneFactory() else {
+ throw(AudioMicrophoneService.AudioError.initializationFailure)
+ }
+ capture = audioKitMicrophone.capture
+ record = audioKitMicrophone.record
+ rxSetup()
+ }
+
+ func stopCapturing() {
+ guard isCapturing else {
+ return
+ }
+ capture.stop()
+ isCapturing = false
+ }
+
+ func startCapturing() {
+ guard !isCapturing else {
+ return
+ }
+ do {
+ try capture.start()
+ } catch {
+ return
+ }
+ isCapturing = true
+ }
+
+ func stopRecording() {
+ guard isRecording else {
+ return
+ }
+ record.stop()
+ isRecording = false
+ guard let audioFile = record.audioFile else {
+ errorSubject.onNext(AudioError.recordFailure)
+ return
+ }
+ directoryDocumentsSavableSubject.onNext(audioFile)
+ }
+
+ func startRecording() {
+ guard !isRecording else {
+ return
+ }
+ do {
+ try record.reset()
+ try record.record()
+ isRecording = true
+ } catch {
+ errorSubject.onNext(AudioError.recordFailure)
+ }
+ }
+
+ private func rxSetup() {
+ capture.bufferReadable.subscribe(onNext: { [unowned self] bufferReadable in
+ self.microphoneBufferReadableSubject.onNext(bufferReadable)
+ }).disposed(by: disposeBag)
+ }
+
+}
+
diff --git a/Baby Monitor/Source Files/Services/CryingDetection/CryingDetectionService.swift b/Baby Monitor/Source Files/Services/CryingDetection/CryingDetectionService.swift
new file mode 100644
index 0000000..624dc0e
--- /dev/null
+++ b/Baby Monitor/Source Files/Services/CryingDetection/CryingDetectionService.swift
@@ -0,0 +1,68 @@
+//
+// CryingDetectionService.swift
+// Baby Monitor
+//
+
+import Foundation
+import CoreML
+import AudioKit
+import RxSwift
+import RxCocoa
+
+protocol CryingDetectionServiceProtocol: Any {
+
+ /// Observable that informs about detection of baby's cry
+ var cryingDetectionObservable: Observable { get }
+
+ /// Starts crying detection
+ func startAnalysis()
+ /// Stops crying detection
+ func stopAnalysis()
+}
+
+final class CryingDetectionService: CryingDetectionServiceProtocol {
+
+ lazy var cryingDetectionObservable = cryingDetectionSubject.asObservable()
+
+ private let cryingDetectionSubject = PublishSubject()
+
+ private var isCryingEventDetected = false
+ private let microphoneCapture: AudioMicrophoneCaptureServiceProtocol?
+ private let disposeBag = DisposeBag()
+ private let audioprocessingModel = audioprocessing()
+ private let crydetectionModel = crydetection()
+
+ init(microphoneCapture: AudioMicrophoneCaptureServiceProtocol?) {
+ self.microphoneCapture = microphoneCapture
+ rxSetup()
+ }
+
+ func startAnalysis() {
+ microphoneCapture?.startCapturing()
+ }
+
+ func stopAnalysis() {
+ microphoneCapture?.stopCapturing()
+ }
+
+ private func rxSetup() {
+ microphoneCapture?.microphoneBufferReadableObservable.subscribe(onNext: { [unowned self] bufferReadable in
+ do {
+ print("RUNNING ML MODEL")
+ let audioProcessingMultiArray = try MLMultiArray(dataPointer: bufferReadable.floatChannelData!.pointee, shape: [264600], dataType: .float32, strides: [1])
+
+ let input = audioprocessingInput(raw_audio__0: audioProcessingMultiArray)
+ let pred = try self.audioprocessingModel.prediction(input: input)
+ let crydetectionMultiArray = try MLMultiArray(shape: [1,1,1,598,64], dataType: .float32)
+ crydetectionMultiArray.dataPointer.copyMemory(from: pred.Mfcc__0.dataPointer, byteCount: 38272*4)
+ let input1 = crydetectionInput(Mfcc__0: crydetectionMultiArray)
+ let pred2 = try self.crydetectionModel.prediction(input: input1)
+ print(pred2.labels_softmax__0)
+
+ self.cryingDetectionSubject.onNext(false)
+ } catch {
+ print("ERROR")
+ }
+ }).disposed(by: disposeBag)
+ }
+}
diff --git a/Baby Monitor/Source Files/Services/Crying/CryingEventService.swift b/Baby Monitor/Source Files/Services/CryingDetection/CryingEventService.swift
similarity index 76%
rename from Baby Monitor/Source Files/Services/Crying/CryingEventService.swift
rename to Baby Monitor/Source Files/Services/CryingDetection/CryingEventService.swift
index 3026d50..497f45b 100644
--- a/Baby Monitor/Source Files/Services/Crying/CryingEventService.swift
+++ b/Baby Monitor/Source Files/Services/CryingDetection/CryingEventService.swift
@@ -29,14 +29,14 @@ final class CryingEventService: CryingEventsServiceProtocol, ErrorProducable {
private let cryingEventPublisher = PublishSubject()
private let errorPublisher = PublishSubject()
private let cryingDetectionService: CryingDetectionServiceProtocol
- private let audioRecordService: AudioRecordServiceProtocol?
+ private let microphoneRecord: AudioMicrophoneRecordServiceProtocol?
private let activityLogEventsRepository: ActivityLogEventsRepositoryProtocol
private let storageService: StorageServerServiceProtocol
private let disposeBag = DisposeBag()
- init(cryingDetectionService: CryingDetectionServiceProtocol, audioRecordService: AudioRecordServiceProtocol?, activityLogEventsRepository: ActivityLogEventsRepositoryProtocol, storageService: StorageServerServiceProtocol) {
+ init(cryingDetectionService: CryingDetectionServiceProtocol, microphoneRecord: AudioMicrophoneRecordServiceProtocol?, activityLogEventsRepository: ActivityLogEventsRepositoryProtocol, storageService: StorageServerServiceProtocol) {
self.cryingDetectionService = cryingDetectionService
- self.audioRecordService = audioRecordService
+ self.microphoneRecord = microphoneRecord
self.activityLogEventsRepository = activityLogEventsRepository
self.storageService = storageService
rxSetup()
@@ -44,39 +44,40 @@ final class CryingEventService: CryingEventsServiceProtocol, ErrorProducable {
func start() throws {
cryingDetectionService.startAnalysis()
- if audioRecordService == nil {
+ if microphoneRecord == nil {
throw CryingEventServiceError.audioRecordServiceError
}
}
func stop() {
cryingDetectionService.stopAnalysis()
- audioRecordService?.stopRecording()
+ //microphoneRecord?.stopRecording()
}
private func rxSetup() {
cryingDetectionService.cryingDetectionObservable.subscribe(onNext: { [unowned self] isBabyCrying in
if isBabyCrying {
+ print("BABY CRYING")
let fileNameSuffix = DateFormatter.fullTimeFormatString(breakCharacter: "_")
self.nextFileName = "crying_".appending(fileNameSuffix).appending(".caf")
- self.audioRecordService?.startRecording()
+ //self.microphoneRecord?.startRecording()
let cryingEventMessage = EventMessage.initWithCryingEvent(value: self.nextFileName)
self.cryingEventPublisher.onNext(cryingEventMessage)
} else {
- guard self.audioRecordService?.isRecording ?? false else {
+ guard self.microphoneRecord?.isRecording ?? false else {
return
}
- self.audioRecordService?.stopRecording()
+ //self.microphoneRecord?.stopRecording()
}
}).disposed(by: disposeBag)
- audioRecordService?.directoryDocumentsSavableObservable.subscribe(onNext: { [unowned self] savableFile in
+ microphoneRecord?.directoryDocumentsSavableObservable.subscribe(onNext: { [unowned self] savableFile in
savableFile.save(withName: self.nextFileName, completion: { [unowned self] result in
switch result {
case .success:
self.storageService.uploadRecordingsToDatabaseIfAllowed()
case .failure(let error):
- self.errorPublisher.onNext(error ?? AudioRecordService.AudioError.saveFailure)
+ self.errorPublisher.onNext(error ?? AudioMicrophoneService.AudioError.saveFailure)
}
})
}).disposed(by: disposeBag)
diff --git a/Baby Monitor/Source Files/Services/MachineLearning/AudioSpectrogramLayer.swift b/Baby Monitor/Source Files/Services/MachineLearning/AudioSpectrogramLayer.swift
new file mode 100644
index 0000000..53db484
--- /dev/null
+++ b/Baby Monitor/Source Files/Services/MachineLearning/AudioSpectrogramLayer.swift
@@ -0,0 +1,97 @@
+//
+// AudioSpectrogram.swift
+// audio_ops
+//
+// Created by Timo Rohner on 15/02/2019.
+// Copyright © 2019 Timo Rohner. All rights reserved.
+//
+
+import Foundation
+import CoreML
+import Accelerate
+
+enum AudioSpectrogramLayerError: Error {
+ case parameterError(String)
+ case opError(String)
+}
+
+func nextPowerOfTwo(value: Int) -> Int {
+ return Int(pow(2, ceil(log2(Double(value)))))
+}
+
+
+@objc(AudioSpectrogramLayer) class AudioSpectrogramLayer: NSObject, MLCustomLayer {
+ let windowSize:Int
+ let stride:Int
+ let magnitudeSquared:Bool
+ let outputChannels:NSNumber
+ let spectogramOp:SpectrogramOp
+
+ required init(parameters: [String : Any]) throws {
+ if let windowSize = parameters["window_size"] as? Int {
+ self.windowSize = windowSize
+ } else {
+ throw AudioSpectrogramLayerError.parameterError("window_size")
+ }
+
+ if let stride = parameters["stride"] as? Int {
+ self.stride = stride
+ } else {
+ throw AudioSpectrogramLayerError.parameterError("stride")
+ }
+
+ if let magnitudeSquared = parameters["magnitude_squared"] as? Bool {
+ self.magnitudeSquared = magnitudeSquared
+ } else {
+ throw AudioSpectrogramLayerError.parameterError("magnitude_squared")
+ }
+
+
+ outputChannels = NSNumber(integerLiteral: 1 + nextPowerOfTwo(value: self.windowSize) / 2)
+
+ spectogramOp = SpectrogramOp(windowLength: windowSize,
+ stepLength: stride,
+ magnitudeSquared: magnitudeSquared)
+ super.init()
+ }
+
+ func setWeightData(_ weights: [Data]) throws {
+ // No weight data for this layer
+ }
+
+ func outputShapes(forInputShapes inputShapes: [[NSNumber]]) throws -> [[NSNumber]] {
+ // Input shape is [1,1,NUM_SAMPLES, 1,1]
+ // This gives us the output shape
+ // [1,1,1,N_TIME_BINS, N_FREQUENCY_BINS]
+ var outputShapesArray = [[NSNumber]]()
+ let inputLength = Int(truncating:inputShapes[0][2])
+ let outputLength = NSNumber(integerLiteral: 1 + (inputLength - self.windowSize) / self.stride)
+ outputShapesArray.append([1,1,1,outputLength, outputChannels])
+ print("AudioSpectrogramLayer: ",#function, inputShapes, " -> ", outputShapesArray)
+ return outputShapesArray
+ }
+
+ func evaluate(inputs: [MLMultiArray], outputs: [MLMultiArray]) throws {
+ try spectogramOp.compute(input: UnsafeMutablePointer(OpaquePointer(inputs[0].dataPointer)),
+ inputLength: inputs[0].count,
+ output: UnsafeMutablePointer(OpaquePointer(outputs[0].dataPointer)))
+ }
+}
+
+// Audio OPS
+// https://github.com/tensorflow/tensorflow/blob/master/tensorflow/core/ops/audio_ops.cc
+
+// AUDIOSPECTROGRAM
+// https://github.com/tensorflow/tensorflow/blob/master/tensorflow/core/kernels/spectrogram_op.cc
+// https://github.com/tensorflow/tensorflow/blob/master/tensorflow/core/kernels/spectrogram.h
+// https://github.com/tensorflow/tensorflow/blob/master/tensorflow/core/kernels/spectrogram.cc
+
+// MFCC
+// https://github.com/tensorflow/tensorflow/blob/master/tensorflow/core/kernels/mfcc.cc
+// https://github.com/tensorflow/tensorflow/blob/master/tensorflow/core/kernels/mfcc.h
+// https://github.com/tensorflow/tensorflow/blob/master/tensorflow/core/kernels/mfcc_dct.cc
+// https://github.com/tensorflow/tensorflow/blob/master/tensorflow/core/kernels/mfcc_dct.h
+// https://github.com/tensorflow/tensorflow/blob/master/tensorflow/core/kernels/mfcc_mel_filterbank.cc
+// https://github.com/tensorflow/tensorflow/blob/master/tensorflow/core/kernels/mfcc_mel_filterbank.h
+// https://github.com/tensorflow/tensorflow/blob/master/tensorflow/core/kernels/mfcc_op.cc
+
diff --git a/Baby Monitor/Source Files/Services/MachineLearning/MfccLayer.swift b/Baby Monitor/Source Files/Services/MachineLearning/MfccLayer.swift
new file mode 100644
index 0000000..645e29f
--- /dev/null
+++ b/Baby Monitor/Source Files/Services/MachineLearning/MfccLayer.swift
@@ -0,0 +1,99 @@
+//
+// AudioSpectrogram.swift
+// audio_ops
+//
+// Created by Timo Rohner on 15/02/2019.
+// Copyright © 2019 Timo Rohner. All rights reserved.
+//
+
+import Foundation
+import CoreML
+import Accelerate
+
+enum MfccLayerError: Error {
+ case parameterError(String)
+ case evaluationError(String)
+}
+
+
+@objc(MfccLayer) class MfccLayer: NSObject, MLCustomLayer {
+ let upperFrequencyLimit:Double
+ let lowerFrequencyLimit:Double
+ let filterbankChannelCount:Int
+ let dctCoefficientCount:Int
+ let sampleRate:Double
+ var mfccOp:MfccOp?
+
+ required init(parameters: [String : Any]) throws {
+ print(#function, parameters)
+ if let sampleRate = parameters["sample_rate"] as? Double {
+ self.sampleRate = sampleRate
+ } else {
+ throw MfccLayerError.parameterError("sample_rate")
+ }
+ if let upperFrequencyLimit = parameters["upper_frequency_limit"] as? Double {
+ self.upperFrequencyLimit = upperFrequencyLimit
+ } else {
+ throw MfccLayerError.parameterError("upper_frequency_limit")
+ }
+
+ if let lowerFrequencyLimit = parameters["lower_frequency_limit"] as? Double {
+ self.lowerFrequencyLimit = lowerFrequencyLimit
+ } else {
+ throw MfccLayerError.parameterError("lower_frequency_limit")
+ }
+
+ if let filterbankChannelCount = parameters["filterbank_channel_count"] as? Int {
+ self.filterbankChannelCount = filterbankChannelCount
+ } else {
+ throw MfccLayerError.parameterError("filterbank_channel_count")
+ }
+
+ if let dctCoefficientCount = parameters["dct_coefficient_count"] as? Int {
+ self.dctCoefficientCount = dctCoefficientCount
+ } else {
+ throw MfccLayerError.parameterError("dct_coefficient_count")
+ }
+
+ mfccOp = MfccOp(upperFrequencyLimit: upperFrequencyLimit,
+ lowerFrequencyLimit: lowerFrequencyLimit,
+ filterbankChannelCount: filterbankChannelCount,
+ dctCoefficientCount: dctCoefficientCount)
+ try mfccOp!.initialize(inputLength: 1025, inputSampleRate: sampleRate)
+
+ super.init()
+ }
+
+ func setWeightData(_ weights: [Data]) throws {
+ print(#function, weights)
+
+ }
+
+ func outputShapes(forInputShapes inputShapes: [[NSNumber]]) throws -> [[NSNumber]] {
+ // Input shape is [1,1,1,N_TIME_BINS, N_FREQUENCY_BINS]
+ // This gives us the output shape
+ // [1,1,N_TIME_BINS, dct_coefficient_count]
+ var outputShapesArray = [[NSNumber]]()
+
+ outputShapesArray.append([1,1, 1, inputShapes[0][3], NSNumber(value:dctCoefficientCount)])
+ print("MfccLayer: ",#function, inputShapes, " -> ", outputShapesArray)
+ return outputShapesArray
+ }
+
+ func evaluate(inputs: [MLMultiArray], outputs: [MLMultiArray]) throws {
+ let nSpectrogramSamples = Int(truncating: inputs[0].shape[3])
+ let nSpectrogramChannels = Int(truncating: inputs[0].shape[4])
+
+
+ let ptrSpectrogramInput = UnsafeMutablePointer(OpaquePointer(inputs[0].dataPointer))
+ let ptrOutput = UnsafeMutablePointer(OpaquePointer(outputs[0].dataPointer))
+
+ for spectrogramIndex in 0.. Bool {
+ self.inputLength = inputLength
+ self.sampleRate = inputSampleRate
+ self.numChannels = outputChannelCount
+
+ let melLow = freqToMel(freq: lowerFrequencyLimit)
+ let melHi = freqToMel(freq: upperFrequencyLimit)
+ let melSpan = melHi - melLow;
+ let melSpacing = melSpan / Double(numChannels + 1)
+
+ centerFrequencies = [Double](repeating: 0.0, count: numChannels+1)
+
+ for i in 0...numChannels {
+ centerFrequencies![i] = melLow + (melSpacing * Double(i + 1));
+ }
+
+ let hzPerSbin = 0.5 * sampleRate / Double(inputLength - 1)
+ startIndex = Int(1.5 + (lowerFrequencyLimit / hzPerSbin))
+ endIndex = Int(upperFrequencyLimit / hzPerSbin)
+
+ bandMapper = [Int](repeating:0, count: self.inputLength)
+
+ var channel = 0
+
+ for i in 0.. endIndex)) {
+ bandMapper![i] = -2;
+ } else {
+ while ((centerFrequencies![channel] < melf) &&
+ (channel < numChannels)) {
+ channel += 1
+ }
+ bandMapper![i] = channel - 1;
+ }
+ }
+
+ weights = [Double](repeating: 0.0, count: self.inputLength)
+ for i in 0.. endIndex)) {
+ weights![i] = 0.0
+ } else {
+ if (channel >= 0) {
+ weights![i] = (centerFrequencies![channel + 1] - freqToMel(freq: Double(i) * hzPerSbin))/(centerFrequencies![channel + 1] - centerFrequencies![channel])
+ } else {
+ weights![i] = (centerFrequencies![0] - freqToMel(freq: Double(i) * hzPerSbin)) / (centerFrequencies![0] - melLow);
+ }
+ }
+ }
+
+ workData = [Float](repeating: 0.0, count: endIndex-startIndex+1)
+
+ initialized = true;
+ return true;
+ }
+
+ func compute(input: UnsafeMutablePointer, output: UnsafeMutablePointer) {
+ for i in startIndex...endIndex {
+ //let spec_val = sqrtf(input.advanced(by: i).pointee)
+ let specVal = input.advanced(by: i).pointee
+ let weighted = specVal * Float((weights![i]))
+ var channel = bandMapper![i]
+ if (channel >= 0) {
+ output.advanced(by: channel).pointee += weighted
+ }
+ channel += 1
+
+ if (channel < numChannels) {
+ output.advanced(by: channel).pointee += specVal - weighted;
+ }
+ }
+ }
+
+ private func freqToMel(freq: Double) -> Double {
+ return 1127.0 * log(1.0 + (freq / 700.0));
+ }
+}
diff --git a/Baby Monitor/Source Files/Services/MachineLearning/MfccOp.swift b/Baby Monitor/Source Files/Services/MachineLearning/MfccOp.swift
new file mode 100644
index 0000000..d40ca03
--- /dev/null
+++ b/Baby Monitor/Source Files/Services/MachineLearning/MfccOp.swift
@@ -0,0 +1,75 @@
+//
+// MfccOp.swift
+// audio_ops
+//
+// Created by Timo Rohner on 17/02/2019.
+// Copyright © 2019 Timo Rohner. All rights reserved.
+//
+
+import Foundation
+import Accelerate
+
+enum MfccOpError :Error {
+ case stringerror(String)
+}
+
+class MfccOp {
+ var initialized:Bool = false
+ let upperFrequencyLimit:Double
+ let lowerFrequencyLimit:Double
+ let filterbankChannelCount:Int
+ let dctCoefficientCount:Int
+ var inputLength:Int = 0
+ var inputSampleRate:Double = 0
+ var kFilterbankFloor:Float = 1e-12
+ //var mfcc_dct:MfccDct?
+ var melFilterbank:MfccMelFilterbank?
+ var dctSetup:vDSP_DFT_Setup?
+
+
+ init(upperFrequencyLimit:Double,
+ lowerFrequencyLimit:Double,
+ filterbankChannelCount:Int,
+ dctCoefficientCount:Int) {
+ self.upperFrequencyLimit = upperFrequencyLimit
+ self.lowerFrequencyLimit = lowerFrequencyLimit
+ self.filterbankChannelCount = filterbankChannelCount
+ self.dctCoefficientCount = dctCoefficientCount
+ //mfcc_dct = MfccDct()
+ }
+
+ func initialize(inputLength: Int, inputSampleRate: Double) throws {
+ dctSetup = vDSP_DCT_CreateSetup(nil, vDSP_Length(dctCoefficientCount), vDSP_DCT_Type.II)
+ self.inputLength = inputLength
+ self.inputSampleRate = inputSampleRate
+
+ melFilterbank = MfccMelFilterbank()
+ if(!melFilterbank!.initialize(inputLength: inputLength, inputSampleRate: inputSampleRate, outputChannelCount: filterbankChannelCount, lowerFrequencyLimit: lowerFrequencyLimit, upperFrequencyLimit: upperFrequencyLimit)) {
+ throw MfccOpError.stringerror("err")
+ }
+
+ //try mfcc_dct!.Initialize(input_length: filterbank_channel_count_, coefficient_count: dct_coefficient_count_)
+
+ self.initialized = true
+ }
+
+ func compute(input: UnsafeMutablePointer, inputLength: Int, output: UnsafeMutablePointer) throws {
+ if(!initialized) {
+ print("ERROR")
+ }
+ var workingData1 = [Float](repeating: 0.0, count: filterbankChannelCount)
+ var workingData2 = [Float](repeating: 0.0, count: filterbankChannelCount)
+ var workingData3 = [Float](repeating: 0.0, count: filterbankChannelCount)
+ melFilterbank!.compute(input: input, output: &workingData1)
+ vDSP_vthr(&workingData1, 1, &kFilterbankFloor, &workingData2, 1, vDSP_Length(filterbankChannelCount))
+
+ var nElements = Int32(filterbankChannelCount)
+ vvlogf(&workingData3, &workingData2, &nElements)
+
+ // Execute DCT
+ var fnorm = sqrtf(2.0 / Float(64))
+ vDSP_vsmul(&workingData3, 1, &fnorm, &workingData2, 1, vDSP_Length(64))
+ vDSP_DCT_Execute(dctSetup!, &workingData2, output)
+ //mfcc_dct!.Compute(input: &working_data3, output: output)
+ }
+}
diff --git a/Baby Monitor/Source Files/Services/MachineLearning/SpectrogramOp.swift b/Baby Monitor/Source Files/Services/MachineLearning/SpectrogramOp.swift
new file mode 100644
index 0000000..c0822a2
--- /dev/null
+++ b/Baby Monitor/Source Files/Services/MachineLearning/SpectrogramOp.swift
@@ -0,0 +1,124 @@
+//
+// Spectrogram.swift
+// audio_ops
+//
+// Created by Timo Rohner on 16/02/2019.
+// Copyright © 2019 Timo Rohner. All rights reserved.
+//
+
+import Foundation
+import Accelerate
+
+// Input: [264600,1]
+// => Bin into windows => [598, 1323]
+// =>
+//
+//
+class SpectrogramOp: NSObject{
+ var stepLength:Int = 0
+ var windowLength:Int = 0
+ var fftLength:Int = 0
+ var outputFrequencyChannels:Int = 0
+ var initialized:Bool = false
+ var scaleFactor : Float = 0.5
+
+ var window:[Float]
+ var inputReal:[Float]
+ var inputImg:[Float]
+ var fftReal:[Float]
+ var fftImg:[Float]
+
+ var samplesToNextStep:Int = 0
+ var fftSetup:vDSP_DFT_Setup?
+ var magnitudeSquared:Bool = true
+
+ init(windowLength: Int, stepLength: Int, magnitudeSquared: Bool) {
+ self.windowLength = windowLength
+ self.stepLength = stepLength
+ self.magnitudeSquared = magnitudeSquared
+
+ if (self.windowLength < 2) {
+ print("window_length too short. window_length has to be greater or equal to 2.")
+ }
+
+ if (self.stepLength < 1) {
+ print("step_length must be strictly positive")
+ }
+
+ fftLength = nextPowerOfTwo(value: self.windowLength)
+ window = [Float](repeating: 0.0, count: self.windowLength)
+
+ inputReal = [Float](repeating: 0.0, count: fftLength/2+1)
+ inputImg = [Float](repeating: 0.0, count: fftLength/2+1)
+
+ fftReal = [Float](repeating: 0.0, count: fftLength/2+1)
+ fftImg = [Float](repeating: 0.0, count: fftLength/2+1)
+
+ vDSP_hann_window(&window, vDSP_Length(windowLength), Int32(vDSP_HANN_DENORM))
+
+ samplesToNextStep = windowLength
+ outputFrequencyChannels = 1 + fftLength / 2
+
+ // Initialize FFT
+ fftSetup = vDSP_DFT_zrop_CreateSetup(nil, vDSP_Length(fftLength), .FORWARD)
+ }
+
+ deinit {
+ vDSP_DFT_DestroySetup(fftSetup)
+ }
+
+ func compute(input: UnsafeMutablePointer,
+ inputLength: Int,
+ output: UnsafeMutablePointer) throws {
+
+ let nTimebins = 1 + (inputLength - windowLength) / stepLength
+ var nSamples = windowLength
+
+ for i in 0.. AudioKitMicrophoneProtocol? = {
+ AKSettings.bufferLength = .medium
+ AKSettings.sampleRate = 44100.0
+ AKSettings.channelCount = 1
+
+ try AKSettings.setSession(category: .playAndRecord, with: .allowBluetoothA2DP)
+ AKSettings.defaultToSpeaker = true
+
+ recorderBooster = AKBooster(microphone)
+ captureBooster = AKBooster(microphone)
+
+ monoToStereo = AKStereoFieldLimiter(recorderBooster, amount: 1)
+ recorderMixer = AKMixer(monoToStereo)
+ recorder = try AKNodeRecorder(node: recorderMixer)
+ capture = try AudioKitNodeCapture(node: captureBooster)
+ recorderSilenceBooster = AKBooster(recorderMixer)
+ recorderSilenceBooster?.gain = 0.0
+ captureSilenceBooster = AKBooster(captureBooster)
+ captureSilenceBooster?.gain = 0.0
+
+ guard let recorder = recorder else {
+ return nil
+ }
+ guard let capture = capture else {
+ return nil
+ }
+ guard let audioFile = recorder.audioFile else {
+ return nil
+ }
+ player = AKPlayer(audioFile: audioFile)
+ mainMixer = AKMixer(player, recorderSilenceBooster)
+ AudioKit.output = mainMixer
+ try AudioKit.start()
+
+ return AudioKitMicrophone(record: recorder, capture: capture)
+ }
+}
+
+protocol MicrophoneRecordProtocol: Any {
+ var audioFile: AKAudioFile? { get }
+
+ func stop()
+ func record() throws
+ func reset() throws
+}
+
+protocol MicrophoneCaptureProtocol: Any {
+ var bufferReadable: Observable { get }
+ var isCapturing: Bool { get }
+
+ func stop()
+ func start() throws
+ func reset() throws
+}
+
+extension AKNodeRecorder: MicrophoneRecordProtocol {}
+extension AudioKitNodeCapture: MicrophoneCaptureProtocol {}
diff --git a/Baby Monitor/Source Files/Services/MediaPlayer/NodeCapture.swift b/Baby Monitor/Source Files/Services/MediaPlayer/NodeCapture.swift
new file mode 100644
index 0000000..ac9d2f2
--- /dev/null
+++ b/Baby Monitor/Source Files/Services/MediaPlayer/NodeCapture.swift
@@ -0,0 +1,111 @@
+//
+// NodeCapture.swift
+// Baby Monitor
+//
+
+
+import Foundation
+import AudioKit
+import RxSwift
+import RxCocoa
+
+class AudioKitNodeCapture: NSObject {
+
+ enum AudioCaptureError: Error {
+ case initializationFailure
+ case captureFailure
+ }
+
+ private let bufferReadableSubject = PublishSubject()
+ lazy var bufferReadable = bufferReadableSubject.asObservable()
+
+ private(set) var isCapturing: Bool = false
+ private var node: AKNode?
+ private let bufferSize: UInt32
+ private var internalAudioBuffer: AVAudioPCMBuffer?
+ private let bufferFormat: AVAudioFormat?
+
+
+ public init(node: AKNode? = AudioKit.output, bufferSize: UInt32 = 264600) throws {
+ self.node = node
+ self.bufferSize = bufferSize
+
+ self.bufferFormat = AVAudioFormat(commonFormat: .pcmFormatFloat32,
+ sampleRate: 44100.0,
+ channels: 1,
+ interleaved: false)
+ guard let bufferFormat = bufferFormat else {
+ throw AudioCaptureError.initializationFailure
+ }
+ self.internalAudioBuffer = AVAudioPCMBuffer(pcmFormat: bufferFormat, frameCapacity: bufferSize)
+ }
+
+ /// Start Capturing
+ func start() throws {
+ guard !isCapturing else {
+ return
+ }
+ guard let node = node else {
+ return
+ }
+
+ guard let internalAudioBuffer = internalAudioBuffer else {
+ return
+ }
+
+ node.avAudioUnitOrNode.installTap(
+ onBus: 0,
+ bufferSize: AKSettings.bufferLength.samplesCount,
+ format: internalAudioBuffer.format) { [weak self] (buffer: AVAudioPCMBuffer!, _) -> Void in
+
+ guard let strongSelf = self else {
+ AKLog("Error: self is nil")
+ return
+ }
+
+ guard let internalAudioBuffer = strongSelf.internalAudioBuffer else {
+ AKLog("Error: internalAudioBuffer is nil")
+ return
+ }
+
+ let samplesLeft = internalAudioBuffer.frameCapacity - internalAudioBuffer.frameLength
+
+ if(buffer.frameLength < samplesLeft) {
+ internalAudioBuffer.copy(from: buffer)
+ }
+ else if(buffer.frameLength >= samplesLeft) {
+ internalAudioBuffer.copy(from: buffer, readOffset: 0, frames: samplesLeft)
+ print("Buffer is filled. Pushing observe event")
+ strongSelf.bufferReadableSubject.onNext(internalAudioBuffer.copy() as! AVAudioPCMBuffer)
+ guard let bufferFormat = strongSelf.bufferFormat else {
+ AKLog("Error: bufferFormat is nil")
+ return
+ }
+ strongSelf.internalAudioBuffer = AVAudioPCMBuffer(pcmFormat: bufferFormat, frameCapacity: strongSelf.bufferSize)
+ }
+ if(buffer.frameLength > samplesLeft) {
+ internalAudioBuffer.copy(from: buffer, readOffset: samplesLeft, frames: buffer.frameLength - samplesLeft)
+ }
+ }
+ isCapturing = true
+
+ }
+
+ /// Stop Capturing
+ func stop() {
+ guard isCapturing else {
+ return
+ }
+ node?.avAudioUnitOrNode.removeTap(onBus: 0)
+ }
+
+ /// Reset the Buffer to clear previous recordings
+ func reset() throws {
+ stop()
+ guard let bufferFormat = bufferFormat else {
+ AKLog("Error: bufferFormat is nil")
+ return
+ }
+ internalAudioBuffer = AVAudioPCMBuffer(pcmFormat: bufferFormat, frameCapacity: bufferSize)
+ }
+}
diff --git a/Baby Monitor/Source Files/Services/MediaPlayer/RecorderFactory.swift b/Baby Monitor/Source Files/Services/MediaPlayer/RecorderFactory.swift
deleted file mode 100644
index 47b2972..0000000
--- a/Baby Monitor/Source Files/Services/MediaPlayer/RecorderFactory.swift
+++ /dev/null
@@ -1,53 +0,0 @@
-//
-// RecorderFactory.swift
-// Baby Monitor
-//
-
-import Foundation
-import AudioKit
-
-enum AudioKitRecorderFactory {
-
- private static var recorder: AKNodeRecorder?
- private static var player: AKPlayer?
- private static var micMixer: AKMixer?
- private static var micBooster: AKBooster?
- private static var moogLadder: AKMoogLadder?
- private static var mainMixer: AKMixer?
- private static let microphone = AKMicrophone()
-
- static var makeRecorderFactory: () -> RecorderProtocol? = {
- // Session settings
- AKSettings.bufferLength = .medium
- do {
- try AKSettings.setSession(category: .playAndRecord, with: .allowBluetoothA2DP)
- } catch {
- return nil
- }
- AKSettings.defaultToSpeaker = true
- // Patching
- let monoToStereo = AKStereoFieldLimiter(microphone, amount: 1)
- micMixer = AKMixer(monoToStereo)
- micBooster = AKBooster(micMixer)
- // Will set the level of microphone monitoring
- micBooster?.gain = 0
- do {
- recorder = try AKNodeRecorder(node: micMixer)
- } catch {
- return nil
- }
- guard let audioFile = recorder?.audioFile else {
- return nil
- }
- player = AKPlayer(audioFile: audioFile)
- moogLadder = AKMoogLadder(player)
- mainMixer = AKMixer(moogLadder, micBooster)
- AudioKit.output = mainMixer
- do {
- try AudioKit.start()
- } catch {
- return nil
- }
- return recorder
- }
-}
diff --git a/PeerConnectionFactoryProtocol.swift b/PeerConnectionFactoryProtocol.swift
index bc55b57..b7857a7 100644
--- a/PeerConnectionFactoryProtocol.swift
+++ b/PeerConnectionFactoryProtocol.swift
@@ -27,8 +27,8 @@ extension RTCPeerConnectionFactory: PeerConnectionFactoryProtocol {
let vTrack = videoTrack(withID: "ARDAMSv0", source: videoSource(with: videoCapturer, constraints: nil))
localStream.addVideoTrack(vTrack)
- let aTrack = audioTrack(withID: "ARDAMSa0")
- localStream.addAudioTrack(aTrack)
+ //let aTrack = audioTrack(withID: "ARDAMSa0")
+ //localStream.addAudioTrack(aTrack)
return localStream
diff --git a/audioprocessing.mlmodel b/audioprocessing.mlmodel
new file mode 100644
index 0000000000000000000000000000000000000000..ecd1cb7bf59c6092cc3887709620a7bf2e2fa509
GIT binary patch
literal 557
zcma)2Jxc>Y5Owd)k4!}3YA{40hozO3pjAZd6fypQW!;;Li+8){-X1|KOFIP{OHFHO
zWgGkrqCdgPaKFG@&>}&>sfIUi-pqS54&8udm^Hk^cos;_*m}LZRs#3#woEs%+lI$`7HVcrYXBjsbQ1`|qIaz>mN2)+~SSl}&dVTT;Ihlj0)EXHKfoHep?CxZ#)v-Sm
z^z8|^zR<>CnrIcIzSRQ^{qge$i7%CJQKj01k?z2a176{r{^FqilS#MnL0YAP_Wk}M
zlv5rj5i-Lq(#bw~d2@2I0@XlRCNzej6cSa@XUbByPntPtKrNJ!MRUy6DHA7LDU3!K
wt5D671Pu!fo@J=SIg4a0EiD~R6m|79kQtB$pZ`s2UN?Kr=D*@8ER9F<1^A4--v9sr
literal 0
HcmV?d00001
diff --git a/crydetection.mlmodel b/crydetection.mlmodel
new file mode 100644
index 0000000000000000000000000000000000000000..7dbc783311e090b5dd4250f4db87f4b615169dff
GIT binary patch
literal 271989
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