Bump
This commit is contained in:
12
.idea/deploymentTargetSelector.xml
generated
12
.idea/deploymentTargetSelector.xml
generated
@@ -4,10 +4,10 @@
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="wear">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2024-09-04T15:12:51.439753961Z">
|
||||
<DropdownSelection timestamp="2024-09-10T00:57:13.348042Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="LocalEmulator" identifier="path=/home/isp/.config/.android/avd/Wear_OS_Large_Round_API_34.avd" />
|
||||
<DeviceId pluginId="LocalEmulator" identifier="path=/Users/isp/.android/avd/Wear_OS_Large_Round_API_34.avd" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
@@ -15,6 +15,14 @@
|
||||
</SelectionState>
|
||||
<SelectionState runConfigName="mobile">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2024-09-10T01:00:38.270417Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="LocalEmulator" identifier="path=/Users/isp/.android/avd/Wear_OS_Large_Round_API_34.avd" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
|
||||
4
.idea/inspectionProfiles/Project_Default.xml
generated
4
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -37,6 +37,10 @@
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
|
||||
1
.idea/misc.xml
generated
1
.idea/misc.xml
generated
@@ -1,3 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||
|
||||
@@ -20,6 +20,8 @@ composeNavigation = "1.4.0-rc01"
|
||||
media3Common = "1.4.0"
|
||||
composeMaterial3 = "1.0.0-alpha23"
|
||||
workRuntimeKtx = "2.9.1"
|
||||
lifecycleRuntimeKtx = "2.6.1"
|
||||
litert = "1.0.1"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
@@ -47,6 +49,9 @@ androidx-compose-navigation = { group = "androidx.wear.compose", name = "compose
|
||||
androidx-media3-common = { group = "androidx.media3", name = "media3-common", version.ref = "media3Common" }
|
||||
androidx-compose-material3 = { group = "androidx.wear.compose", name = "compose-material3", version.ref = "composeMaterial3" }
|
||||
androidx-work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "workRuntimeKtx" }
|
||||
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
|
||||
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||
litert = { group = "com.google.ai.edge.litert", name = "litert", version.ref = "litert" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
}
|
||||
|
||||
android {
|
||||
@@ -33,6 +34,9 @@ android {
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -42,8 +46,22 @@ dependencies {
|
||||
implementation(libs.material)
|
||||
implementation(libs.androidx.activity)
|
||||
implementation(libs.androidx.constraintlayout)
|
||||
implementation(libs.androidx.work.runtime.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.ui)
|
||||
implementation("uk.me.berndporr:iirj:1.7")
|
||||
implementation(libs.androidx.ui.graphics)
|
||||
implementation(libs.androidx.ui.tooling.preview)
|
||||
implementation(libs.androidx.material3)
|
||||
implementation(libs.litert)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||
androidTestImplementation(libs.androidx.ui.test.junit4)
|
||||
debugImplementation(libs.androidx.ui.tooling)
|
||||
debugImplementation(libs.androidx.ui.test.manifest)
|
||||
wearApp(project(":wear"))
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
xmlns:tools="http://schemas.android.com/tools" >
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"></uses-permission>
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"></uses-permission>
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"></uses-permission>
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
@@ -11,16 +15,31 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Identify"
|
||||
tools:targetApi="31">
|
||||
tools:targetApi="31" >
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
android:exported="true" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".MessageListenerService"
|
||||
android:enabled="true"
|
||||
android:exported="true" >
|
||||
<intent-filter>
|
||||
<action android:name="com.google.android.gms.wearable.MESSAGE_RECEIVED" />
|
||||
|
||||
<data
|
||||
android:host="*"
|
||||
android:pathPrefix="/audio"
|
||||
android:scheme="wear" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
6522
mobile/src/main/assets/assets.txt
Normal file
6522
mobile/src/main/assets/assets.txt
Normal file
File diff suppressed because it is too large
Load Diff
6522
mobile/src/main/assets/labels_en.txt
Normal file
6522
mobile/src/main/assets/labels_en.txt
Normal file
File diff suppressed because it is too large
Load Diff
6522
mobile/src/main/assets/taxo_code.txt
Normal file
6522
mobile/src/main/assets/taxo_code.txt
Normal file
File diff suppressed because it is too large
Load Diff
189
mobile/src/main/java/com/birdsounds/identify/Downloader.java
Normal file
189
mobile/src/main/java/com/birdsounds/identify/Downloader.java
Normal file
@@ -0,0 +1,189 @@
|
||||
package com.birdsounds.identify;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
public class Downloader {
|
||||
static final String modelFILE = "model.tflite";
|
||||
static final String metaModelFILE = "metaModel.tflite";
|
||||
static final String modelURL = "https://raw.githubusercontent.com/woheller69/whoBIRD-TFlite/master/BirdNET_GLOBAL_6K_V2.4_Model_FP16.tflite";
|
||||
static final String model32URL = "https://raw.githubusercontent.com/woheller69/whoBIRD-TFlite/master/BirdNET_GLOBAL_6K_V2.4_Model_FP32.tflite";
|
||||
static final String metaModelURL = "https://raw.githubusercontent.com/woheller69/whoBIRD-TFlite/master/BirdNET_GLOBAL_6K_V2.4_MData_Model_FP16.tflite";
|
||||
static final String modelMD5 = "b1c981fe261910b473b9b7eec9ebcd4e";
|
||||
static final String model32MD5 = "6c7c42106e56550fc8563adb31bc120e";
|
||||
static final String metaModelMD5 ="f1a078ae0f244a1ff5a8f1ccb645c805";
|
||||
|
||||
public static boolean checkModels(final Activity activity) {
|
||||
File modelFile = new File(activity.getDir("filesdir", Context.MODE_PRIVATE) + "/" + modelFILE);
|
||||
File metaModelFile = new File(activity.getDir("filesdir", Context.MODE_PRIVATE) + "/" + metaModelFILE);
|
||||
String calcModelMD5 = "";
|
||||
String calcMetaModelMD5 = "";
|
||||
if (modelFile.exists()) {
|
||||
try {
|
||||
byte[] data = Files.readAllBytes(Paths.get(modelFile.getPath()));
|
||||
byte[] hash = MessageDigest.getInstance("MD5").digest(data);
|
||||
calcModelMD5 = new BigInteger(1, hash).toString(16);
|
||||
} catch (IOException | NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
if (metaModelFile.exists()) {
|
||||
try {
|
||||
byte[] data = Files.readAllBytes(Paths.get(metaModelFile.getPath()));
|
||||
byte[] hash = MessageDigest.getInstance("MD5").digest(data);
|
||||
calcMetaModelMD5 = new BigInteger(1, hash).toString(16);
|
||||
} catch (IOException | NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (modelFile.exists() && !(calcModelMD5.equals(modelMD5) || calcModelMD5.equals(model32MD5))) modelFile.delete();
|
||||
if (metaModelFile.exists() && !calcMetaModelMD5.equals(metaModelMD5)) metaModelFile.delete();
|
||||
|
||||
return (calcModelMD5.equals(modelMD5) || calcModelMD5.equals(model32MD5)) && calcMetaModelMD5.equals(metaModelMD5);
|
||||
}
|
||||
|
||||
public static void downloadModels(final Activity activity) {
|
||||
File modelFile = new File(activity.getDir("filesdir", Context.MODE_PRIVATE) + "/" + modelFILE);
|
||||
Log.d("Heyy","Model file checking");
|
||||
if (!modelFile.exists()) {
|
||||
Log.d("whoBIRD", "model file does not exist");
|
||||
Thread thread = new Thread(() -> {
|
||||
try {
|
||||
URL url;
|
||||
if (false) url = new URL(model32URL);
|
||||
else url = new URL(modelURL);
|
||||
|
||||
Log.d("whoBIRD", "Download model");
|
||||
|
||||
URLConnection ucon = url.openConnection();
|
||||
ucon.setReadTimeout(5000);
|
||||
ucon.setConnectTimeout(10000);
|
||||
|
||||
InputStream is = ucon.getInputStream();
|
||||
BufferedInputStream inStream = new BufferedInputStream(is, 1024 * 5);
|
||||
|
||||
modelFile.createNewFile();
|
||||
|
||||
FileOutputStream outStream = new FileOutputStream(modelFile);
|
||||
byte[] buff = new byte[5 * 1024];
|
||||
|
||||
int len;
|
||||
while ((len = inStream.read(buff)) != -1) {
|
||||
outStream.write(buff, 0, len);
|
||||
}
|
||||
outStream.flush();
|
||||
outStream.close();
|
||||
inStream.close();
|
||||
|
||||
String calcModelMD5="";
|
||||
if (modelFile.exists()) {
|
||||
byte[] data = Files.readAllBytes(Paths.get(modelFile.getPath()));
|
||||
byte[] hash = MessageDigest.getInstance("MD5").digest(data);
|
||||
calcModelMD5 = new BigInteger(1, hash).toString(16);
|
||||
} else {
|
||||
throw new IOException(); //throw exception if there is no modelFile at this point
|
||||
}
|
||||
|
||||
if (!(calcModelMD5.equals(modelMD5) || calcModelMD5.equals(model32MD5) )){
|
||||
modelFile.delete();
|
||||
activity.runOnUiThread(() -> {
|
||||
Toast.makeText(activity, activity.getResources().getString(R.string.error_download), Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
} else {
|
||||
activity.runOnUiThread(() -> {
|
||||
|
||||
});
|
||||
}
|
||||
} catch (NoSuchAlgorithmException | IOException i) {
|
||||
activity.runOnUiThread(() -> Toast.makeText(activity, activity.getResources().getString(R.string.error_download), Toast.LENGTH_SHORT).show());
|
||||
modelFile.delete();
|
||||
Log.w("whoBIRD", activity.getResources().getString(R.string.error_download), i);
|
||||
}
|
||||
});
|
||||
thread.start();
|
||||
} else {
|
||||
Log.d("whoBIRD","model exists");
|
||||
activity.runOnUiThread(() -> {
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
File metaModelFile = new File(activity.getDir("filesdir", Context.MODE_PRIVATE) + "/" + metaModelFILE);
|
||||
if (!metaModelFile.exists()) {
|
||||
Log.d("whoBIRD", "meta model file does not exist");
|
||||
Thread thread = new Thread(() -> {
|
||||
try {
|
||||
URL url = new URL(metaModelURL);
|
||||
Log.d("whoBIRD", "Download meta model");
|
||||
|
||||
URLConnection ucon = url.openConnection();
|
||||
ucon.setReadTimeout(5000);
|
||||
ucon.setConnectTimeout(10000);
|
||||
|
||||
InputStream is = ucon.getInputStream();
|
||||
BufferedInputStream inStream = new BufferedInputStream(is, 1024 * 5);
|
||||
|
||||
metaModelFile.createNewFile();
|
||||
|
||||
FileOutputStream outStream = new FileOutputStream(metaModelFile);
|
||||
byte[] buff = new byte[5 * 1024];
|
||||
|
||||
int len;
|
||||
while ((len = inStream.read(buff)) != -1) {
|
||||
outStream.write(buff, 0, len);
|
||||
}
|
||||
outStream.flush();
|
||||
outStream.close();
|
||||
inStream.close();
|
||||
|
||||
String calcMetaModelMD5="";
|
||||
if (metaModelFile.exists()) {
|
||||
byte[] data = Files.readAllBytes(Paths.get(metaModelFile.getPath()));
|
||||
byte[] hash = MessageDigest.getInstance("MD5").digest(data);
|
||||
calcMetaModelMD5 = new BigInteger(1, hash).toString(16);
|
||||
} else {
|
||||
throw new IOException(); //throw exception if there is no modelFile at this point
|
||||
}
|
||||
|
||||
if (!calcMetaModelMD5.equals(metaModelMD5)){
|
||||
metaModelFile.delete();
|
||||
activity.runOnUiThread(() -> {
|
||||
Toast.makeText(activity, activity.getResources().getString(R.string.error_download), Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
} else {
|
||||
activity.runOnUiThread(() -> {
|
||||
});
|
||||
}
|
||||
} catch (NoSuchAlgorithmException | IOException i) {
|
||||
activity.runOnUiThread(() -> Toast.makeText(activity, activity.getResources().getString(R.string.error_download), Toast.LENGTH_SHORT).show());
|
||||
metaModelFile.delete();
|
||||
Log.w("whoBIRD", activity.getResources().getString(R.string.error_download), i);
|
||||
}
|
||||
});
|
||||
thread.start();
|
||||
} else {
|
||||
Log.d("whoBIRD", "meta file exists");
|
||||
activity.runOnUiThread(() -> {
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
59
mobile/src/main/java/com/birdsounds/identify/Location.java
Normal file
59
mobile/src/main/java/com/birdsounds/identify/Location.java
Normal file
@@ -0,0 +1,59 @@
|
||||
package com.birdsounds.identify;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.location.LocationListener;
|
||||
import android.location.LocationManager;
|
||||
import android.os.Bundle;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.core.app.ActivityCompat;
|
||||
|
||||
public class Location {
|
||||
|
||||
private static LocationListener locationListenerGPS;
|
||||
|
||||
static void stopLocation(Context context){
|
||||
LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
|
||||
if (locationListenerGPS!=null) locationManager.removeUpdates(locationListenerGPS);
|
||||
locationListenerGPS=null;
|
||||
}
|
||||
|
||||
static void requestLocation(Context context, SoundClassifier soundClassifier) {
|
||||
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED && checkLocationProvider(context)) {
|
||||
LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
|
||||
if (locationListenerGPS==null) locationListenerGPS = new LocationListener() {
|
||||
@Override
|
||||
public void onLocationChanged(android.location.Location location) {
|
||||
soundClassifier.runMetaInterpreter(location);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@Override
|
||||
public void onStatusChanged(String provider, int status, Bundle extras) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProviderEnabled(String provider) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProviderDisabled(String provider) {
|
||||
}
|
||||
};
|
||||
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 60000, 0, locationListenerGPS);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean checkLocationProvider(Context context) {
|
||||
LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
|
||||
if (!locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)){
|
||||
Toast.makeText(context, "Error no GPS", Toast.LENGTH_SHORT).show();
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,20 +1,57 @@
|
||||
package com.birdsounds.identify
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.Manifest
|
||||
import android.util.Log
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import com.google.android.gms.wearable.ChannelClient
|
||||
import com.google.android.gms.wearable.Wearable
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
private lateinit var soundClassifier: SoundClassifier
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
setContentView(R.layout.activity_main)
|
||||
Wearable.getChannelClient(this.applicationContext)
|
||||
.registerChannelCallback(object : ChannelClient.ChannelCallback() {
|
||||
override fun onChannelOpened(channel: ChannelClient.Channel) {
|
||||
super.onChannelOpened(channel)
|
||||
Log.d("HEY", "onChannelOpened")
|
||||
}
|
||||
}
|
||||
)
|
||||
Downloader.downloadModels(this);
|
||||
requestPermissions();
|
||||
soundClassifier = SoundClassifier(this, SoundClassifier.Options())
|
||||
Location.requestLocation(this, soundClassifier)
|
||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
|
||||
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
||||
insets
|
||||
}
|
||||
}
|
||||
companion object {
|
||||
const val REQUEST_PERMISSIONS = 1337
|
||||
}
|
||||
|
||||
private fun requestPermissions() {
|
||||
val perms = mutableListOf<String>()
|
||||
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
this,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
perms.add(Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||
perms.add(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
}
|
||||
if (!perms.isEmpty()) requestPermissions(perms.toTypedArray(), REQUEST_PERMISSIONS)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.birdsounds.identify
|
||||
import android.content.Intent
|
||||
|
||||
object MessageConstants {
|
||||
const val intentName = "WearableMessageDisplay"
|
||||
const val message = "Message"
|
||||
const val path = "audio"
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.birdsounds.identify
|
||||
|
||||
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import com.google.android.gms.wearable.MessageEvent
|
||||
import com.google.android.gms.wearable.WearableListenerService
|
||||
|
||||
class MessageListenerService : WearableListenerService() {
|
||||
private val tag = "MessageListenerService"
|
||||
override fun onMessageReceived(p0: MessageEvent) {
|
||||
super.onMessageReceived(p0)
|
||||
|
||||
// Log.i(tag ,p0.data.toString(Charsets.UTF_8))
|
||||
// Log.i(tag, p0.data.toString(Charsets.US_ASCII))
|
||||
// broadcastMessage(p0)
|
||||
}
|
||||
|
||||
}
|
||||
388
mobile/src/main/java/com/birdsounds/identify/SoundClassifier.kt
Normal file
388
mobile/src/main/java/com/birdsounds/identify/SoundClassifier.kt
Normal file
@@ -0,0 +1,388 @@
|
||||
package com.birdsounds.identify
|
||||
|
||||
import android.content.Context
|
||||
import android.location.Location
|
||||
import android.os.SystemClock
|
||||
import android.preference.PreferenceManager
|
||||
import android.util.Log
|
||||
import org.tensorflow.lite.Interpreter
|
||||
import java.io.BufferedReader
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStreamReader
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.FloatBuffer
|
||||
import java.nio.channels.FileChannel
|
||||
import java.nio.file.StandardOpenOption
|
||||
import java.time.LocalDate
|
||||
import java.util.Locale
|
||||
import java.util.TimerTask
|
||||
import java.util.Timer
|
||||
import kotlin.concurrent.scheduleAtFixedRate
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.cos
|
||||
import uk.me.berndporr.iirj.Butterworth
|
||||
import kotlin.math.round
|
||||
import kotlin.math.sin
|
||||
|
||||
class SoundClassifier(
|
||||
context: Context,
|
||||
private val options: Options = Options()
|
||||
) {
|
||||
internal var mContext: Context
|
||||
val TAG = "Sound Classifier"
|
||||
init {
|
||||
this.mContext = context.applicationContext
|
||||
}
|
||||
|
||||
class Options(
|
||||
/** Path of the converted model label file, relative to the assets/ directory. */
|
||||
val labelsBase: String = "labels",
|
||||
/** Path of the converted .tflite file, relative to the assets/ directory. */
|
||||
val assetFile: String = "assets.txt",
|
||||
/** Path of the converted .tflite file, relative to the assets/ directory. */
|
||||
val modelPath: String = "model.tflite",
|
||||
/** Path of the meta model .tflite file, relative to the assets/ directory. */
|
||||
val metaModelPath: String = "metaModel.tflite",
|
||||
/** The required audio sample rate in Hz. */
|
||||
val sampleRate: Int = 48000,
|
||||
/** Multiplier for audio samples */
|
||||
val warmupRuns: Int = 3,
|
||||
/** Probability value above which a class in the meta model is labeled as active (i.e., detected) the display. (default 0.01) */
|
||||
var metaProbabilityThreshold1: Float = 0.01f, //min must be > 0
|
||||
var metaProbabilityThreshold2: Float = 0.008f, //min must be > 0
|
||||
var metaProbabilityThreshold3: Float = 0.001f, //min must be > 0
|
||||
/** Probability value above which a class is shown as image. (default 0.5) */
|
||||
var displayImageThreshold: Float = 0.65f, //min must be > 0
|
||||
)
|
||||
|
||||
|
||||
/** Names of the model's output classes. */
|
||||
lateinit var labelList: List<String>
|
||||
|
||||
/** Names of the model's output classes. */
|
||||
lateinit var assetList: List<String>
|
||||
|
||||
/** How many milliseconds between consecutive model inference calls. */
|
||||
private var inferenceInterval = 800L
|
||||
|
||||
/** The TFLite interpreter instance. */
|
||||
private lateinit var interpreter: Interpreter
|
||||
private lateinit var meta_interpreter: Interpreter
|
||||
|
||||
/** Audio length (in # of PCM samples) required by the TFLite model. */
|
||||
private var modelInputLength = 0
|
||||
|
||||
/** input Length of the meta model */
|
||||
private var metaModelInputLength = 0
|
||||
|
||||
/** Number of output classes of the TFLite model. */
|
||||
private var modelNumClasses = 0
|
||||
private var metaModelNumClasses = 0
|
||||
|
||||
|
||||
/** Used to hold the real-time probabilities predicted by the model for the output classes. */
|
||||
private lateinit var predictionProbs: FloatArray
|
||||
private lateinit var metaPredictionProbs: FloatArray
|
||||
|
||||
/** Latest prediction latency in milliseconds. */
|
||||
private var latestPredictionLatencyMs = 0f
|
||||
|
||||
private var recognitionTask: TimerTask? = null
|
||||
|
||||
|
||||
/** Buffer that holds audio PCM sample that are fed to the TFLite model for inference. */
|
||||
private lateinit var inputBuffer: FloatBuffer
|
||||
private lateinit var metaInputBuffer: FloatBuffer
|
||||
|
||||
init {
|
||||
loadLabels(context)
|
||||
loadAssetList(context)
|
||||
setupInterpreter(context)
|
||||
setupMetaInterpreter(context)
|
||||
warmUpModel()
|
||||
}
|
||||
|
||||
|
||||
/** Retrieve asset list from "asset_list" file */
|
||||
private fun loadAssetList(context: Context) {
|
||||
|
||||
try {
|
||||
val reader =
|
||||
BufferedReader(InputStreamReader(context.assets.open(options.assetFile)))
|
||||
val wordList = mutableListOf<String>()
|
||||
reader.useLines { lines ->
|
||||
lines.forEach {
|
||||
wordList.add(it.trim())
|
||||
}
|
||||
}
|
||||
assetList = wordList.map { it }
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Failed to read labels ${options.assetFile}: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
/** Retrieve labels from "labels.txt" file */
|
||||
private fun loadLabels(context: Context) {
|
||||
val localeList = context.resources.configuration.locales
|
||||
val language = localeList.get(0).language
|
||||
var filename = options.labelsBase + "_${language}.txt"
|
||||
|
||||
//Check if file exists
|
||||
val assetManager = context.assets // Replace 'assets' with actual AssetManager instance
|
||||
try {
|
||||
val mapList = assetManager.list("")?.toMutableList()
|
||||
|
||||
if (mapList != null) {
|
||||
if (!mapList.contains(filename)) {
|
||||
filename = options.labelsBase + "_en.txt"
|
||||
}
|
||||
}
|
||||
} catch (ex: IOException) {
|
||||
ex.printStackTrace()
|
||||
filename = options.labelsBase + "_en.txt"
|
||||
}
|
||||
|
||||
Log.i(TAG, filename)
|
||||
try {
|
||||
val reader =
|
||||
BufferedReader(InputStreamReader(context.assets.open(filename)))
|
||||
val wordList = mutableListOf<String>()
|
||||
reader.useLines { lines ->
|
||||
lines.forEach {
|
||||
wordList.add(it)
|
||||
}
|
||||
}
|
||||
labelList = wordList.map { it.toTitleCase() }
|
||||
Log.i(TAG, "Label list entries: ${labelList.size}")
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Failed to read labels ${filename}: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun setupInterpreter(context: Context) {
|
||||
try {
|
||||
val modelFilePath = context.getDir("filesdir", Context.MODE_PRIVATE).absolutePath + "/"+ options.modelPath
|
||||
Log.i(TAG, "Trying to create TFLite buffer from $modelFilePath")
|
||||
val modelFile = File(modelFilePath)
|
||||
val tfliteBuffer: ByteBuffer = FileChannel.open(modelFile.toPath(), StandardOpenOption.READ).use { channel ->
|
||||
channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size())
|
||||
}
|
||||
Log.i(TAG, "Done creating TFLite buffer from $modelFilePath")
|
||||
|
||||
interpreter = Interpreter(tfliteBuffer, Interpreter.Options())
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Failed to load TFLite model - ${e.message}")
|
||||
return
|
||||
}
|
||||
// Inspect input and output specs.
|
||||
val inputShape = interpreter.getInputTensor(0).shape()
|
||||
Log.i(TAG, "TFLite model input shape: ${inputShape.contentToString()}")
|
||||
modelInputLength = inputShape[1]
|
||||
|
||||
val outputShape = interpreter.getOutputTensor(0).shape()
|
||||
Log.i(TAG, "TFLite output shape: ${outputShape.contentToString()}")
|
||||
modelNumClasses = outputShape[1]
|
||||
if (modelNumClasses != labelList.size) {
|
||||
Log.e(
|
||||
TAG,
|
||||
"Mismatch between metadata number of classes (${labelList.size})" +
|
||||
" and model output length ($modelNumClasses)"
|
||||
)
|
||||
}
|
||||
// Fill the array with NaNs initially.
|
||||
predictionProbs = FloatArray(modelNumClasses) { Float.NaN }
|
||||
|
||||
inputBuffer = FloatBuffer.allocate(modelInputLength)
|
||||
|
||||
}
|
||||
|
||||
private fun setupMetaInterpreter(context: Context) {
|
||||
|
||||
try {
|
||||
val metaModelFilePath = context.getDir("filesdir", Context.MODE_PRIVATE).absolutePath + "/"+ options.metaModelPath
|
||||
Log.i(TAG, "Trying to create TFLite buffer from $metaModelFilePath")
|
||||
val metaModelFile = File(metaModelFilePath)
|
||||
val tfliteBuffer: ByteBuffer = FileChannel.open(metaModelFile.toPath(), StandardOpenOption.READ).use { channel ->
|
||||
channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size())
|
||||
}
|
||||
Log.i(TAG, "Done creating TFLite buffer from $metaModelFilePath")
|
||||
|
||||
meta_interpreter = Interpreter(tfliteBuffer, Interpreter.Options())
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Failed to load TFLite model - ${e.message}")
|
||||
return
|
||||
}
|
||||
// Inspect input and output specs.
|
||||
val metaInputShape = meta_interpreter.getInputTensor(0).shape()
|
||||
Log.i(TAG, "TFLite meta model input shape: ${metaInputShape.contentToString()}")
|
||||
metaModelInputLength = metaInputShape[1]
|
||||
|
||||
val metaOutputShape = meta_interpreter.getOutputTensor(0).shape()
|
||||
Log.i(TAG, "TFLite meta model output shape: ${metaOutputShape.contentToString()}")
|
||||
metaModelNumClasses = metaOutputShape[1]
|
||||
if (metaModelNumClasses != labelList.size) {
|
||||
Log.e(
|
||||
TAG,
|
||||
"Mismatch between metadata number of classes (${labelList.size})" +
|
||||
" and meta model output length ($metaModelNumClasses)"
|
||||
)
|
||||
}
|
||||
// Fill the array with 1 initially.
|
||||
metaPredictionProbs = FloatArray(metaModelNumClasses) { 1f }
|
||||
metaInputBuffer = FloatBuffer.allocate(metaModelInputLength)
|
||||
|
||||
}
|
||||
|
||||
fun runMetaInterpreter(location: Location) {
|
||||
val dayOfYear = LocalDate.now().dayOfYear
|
||||
val week = ceil( dayOfYear*48.0/366.0) //model year has 48 weeks
|
||||
lat = location.latitude.toFloat()
|
||||
lon = location.longitude.toFloat()
|
||||
|
||||
val weekMeta = cos(Math.toRadians(week * 7.5)) + 1.0
|
||||
|
||||
metaInputBuffer.put(0, lat)
|
||||
metaInputBuffer.put(1, lon)
|
||||
metaInputBuffer.put(2, weekMeta.toFloat())
|
||||
metaInputBuffer.rewind() // Reset position to beginning of buffer
|
||||
val metaOutputBuffer = FloatBuffer.allocate(metaModelNumClasses)
|
||||
metaOutputBuffer.rewind()
|
||||
meta_interpreter.run(metaInputBuffer, metaOutputBuffer)
|
||||
metaOutputBuffer.rewind()
|
||||
metaOutputBuffer.get(metaPredictionProbs) // Copy data to metaPredictionProbs.
|
||||
|
||||
|
||||
for (i in metaPredictionProbs.indices) {
|
||||
metaPredictionProbs[i] =
|
||||
if (metaPredictionProbs[i] >= options.metaProbabilityThreshold1) {
|
||||
1f
|
||||
} else if (metaPredictionProbs[i] >= options.metaProbabilityThreshold2) {
|
||||
0.8f
|
||||
} else if (metaPredictionProbs[i] >= options.metaProbabilityThreshold3) {
|
||||
0.5f
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun warmUpModel() {
|
||||
generateDummyAudioInput(inputBuffer)
|
||||
for (n in 0 until options.warmupRuns) {
|
||||
|
||||
// Create input and output buffers.
|
||||
val outputBuffer = FloatBuffer.allocate(modelNumClasses)
|
||||
inputBuffer.rewind()
|
||||
outputBuffer.rewind()
|
||||
interpreter.run(inputBuffer, outputBuffer)
|
||||
|
||||
}
|
||||
}
|
||||
private fun generateDummyAudioInput(inputBuffer: FloatBuffer) {
|
||||
val twoPiTimesFreq = 2 * Math.PI.toFloat() * 1000f
|
||||
for (i in 0 until modelInputLength) {
|
||||
val x = i.toFloat() / (modelInputLength - 1)
|
||||
inputBuffer.put(i, sin(twoPiTimesFreq * x.toDouble()).toFloat())
|
||||
}
|
||||
}
|
||||
private fun String.toTitleCase() =
|
||||
splitToSequence("_")
|
||||
.map { it.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.ROOT) else it.toString() } }
|
||||
.joinToString("_")
|
||||
.trim()
|
||||
|
||||
companion object {
|
||||
private const val TAG = "SoundClassifier"
|
||||
var lat: Float = 0.0f
|
||||
var lon: Float = 0.0f
|
||||
/** Number of nanoseconds in a millisecond */
|
||||
private const val NANOS_IN_MILLIS = 1_000_000.toDouble()
|
||||
}
|
||||
|
||||
private fun startRecognition() {
|
||||
if (modelInputLength <= 0 || modelNumClasses <= 0) {
|
||||
Log.e(TAG, "Switches: Cannot start recognition because model is unavailable.")
|
||||
return
|
||||
}
|
||||
val sharedPref = PreferenceManager.getDefaultSharedPreferences(mContext)
|
||||
val highPass = sharedPref.getInt("high_pass",0)
|
||||
val butterworth = Butterworth()
|
||||
butterworth.highPass(6, 48000.0, highPass.toDouble())
|
||||
|
||||
val circularBuffer = ShortArray(modelInputLength)
|
||||
|
||||
var j = 0 // Indices for the circular buffer next write
|
||||
|
||||
Log.w(TAG, "recognitionPeriod:"+inferenceInterval)
|
||||
recognitionTask = Timer().scheduleAtFixedRate(inferenceInterval, inferenceInterval) task@{
|
||||
val outputBuffer = FloatBuffer.allocate(modelNumClasses)
|
||||
val recordingBuffer = ShortArray(modelInputLength)
|
||||
|
||||
// Load new audio samples
|
||||
// val sampleCounts = loadAudio(recordingBuffer)
|
||||
val sampleCounts = 0;
|
||||
if (sampleCounts == 0) {
|
||||
return@task
|
||||
}
|
||||
|
||||
|
||||
// Copy new data into the circular buffer
|
||||
for (i in 0 until sampleCounts) {
|
||||
circularBuffer[j] = recordingBuffer[i]
|
||||
j = (j + 1) % circularBuffer.size
|
||||
}
|
||||
|
||||
// Feed data to the input buffer.
|
||||
var samplesAreAllZero = true
|
||||
for (i in 0 until modelInputLength) {
|
||||
val s = circularBuffer[(i + j) % modelInputLength]
|
||||
if (samplesAreAllZero && s.toInt() != 0) {
|
||||
samplesAreAllZero = false
|
||||
}
|
||||
if (highPass==0) inputBuffer.put(i, s.toFloat())
|
||||
else inputBuffer.put(i, butterworth.filter(s.toDouble()).toFloat())
|
||||
}
|
||||
|
||||
if (samplesAreAllZero) {
|
||||
Log.w(TAG, mContext.resources.getString(R.string.samples_zero))
|
||||
|
||||
|
||||
return@task
|
||||
}
|
||||
|
||||
val t0 = SystemClock.elapsedRealtimeNanos()
|
||||
inputBuffer.rewind()
|
||||
outputBuffer.rewind()
|
||||
interpreter.run(inputBuffer, outputBuffer)
|
||||
outputBuffer.rewind()
|
||||
outputBuffer.get(predictionProbs) // Copy data to predictionProbs.
|
||||
|
||||
val probList = mutableListOf<Float>()
|
||||
// if (mBinding.checkIgnoreMeta.isChecked){
|
||||
// for (value in predictionProbs) {
|
||||
// probList.add(1 / (1 + kotlin.math.exp(-value))) //apply sigmoid
|
||||
// }
|
||||
// } else {
|
||||
for (i in predictionProbs.indices) {
|
||||
probList.add( metaPredictionProbs[i] / (1+kotlin.math.exp(-predictionProbs[i])) ) //apply sigmoid
|
||||
}
|
||||
// }
|
||||
|
||||
// if (mBinding.progressHorizontal.isIndeterminate){ //if start/stop button set to "running"
|
||||
// probList.withIndex().also {
|
||||
// val max = it.maxByOrNull { entry -> entry.value }
|
||||
// updateTextView(max, mBinding.text1)
|
||||
// updateImage(max)
|
||||
//after finding the maximum probability and its corresponding label (max), we filter out that entry from the list of entries before finding the second highest probability (secondMax)
|
||||
// val secondMax = it.filterNot { entry -> entry == max }.maxByOrNull { entry -> entry.value }
|
||||
// updateTextView(secondMax,mBinding.text2)
|
||||
// }
|
||||
// }
|
||||
|
||||
latestPredictionLatencyMs =
|
||||
((SystemClock.elapsedRealtimeNanos() - t0) / 1e6).toFloat()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.birdsounds.identify.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple80 = Color(0xFFD0BCFF)
|
||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
val Pink80 = Color(0xFFEFB8C8)
|
||||
|
||||
val Purple40 = Color(0xFF6650a4)
|
||||
val PurpleGrey40 = Color(0xFF625b71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.birdsounds.identify.ui.theme
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
secondary = PurpleGrey80,
|
||||
tertiary = Pink80
|
||||
)
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = Purple40,
|
||||
secondary = PurpleGrey40,
|
||||
tertiary = Pink40
|
||||
|
||||
/* Other default colors to override
|
||||
background = Color(0xFFFFFBFE),
|
||||
surface = Color(0xFFFFFBFE),
|
||||
onPrimary = Color.White,
|
||||
onSecondary = Color.White,
|
||||
onTertiary = Color.White,
|
||||
onBackground = Color(0xFF1C1B1F),
|
||||
onSurface = Color(0xFF1C1B1F),
|
||||
*/
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun IdentifyTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
// Dynamic color is available on Android 12+
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.birdsounds.identify.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
// Set of Material typography styles to start with
|
||||
val Typography = Typography(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
/* Other default text styles to override
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
*/
|
||||
)
|
||||
@@ -1,3 +1,6 @@
|
||||
<resources>
|
||||
<string name="app_name">identify</string>
|
||||
<string name="title_activity_download">DownloadActivity</string>
|
||||
<string name="error_download">Download Error</string>
|
||||
<string name="samples_zero">Samples are all zero</string>
|
||||
</resources>
|
||||
@@ -47,8 +47,12 @@ val Any.TAG: String
|
||||
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
installSplashScreen()
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
setTheme(android.R.style.Theme_DeviceDefault)
|
||||
setContent {
|
||||
@@ -111,6 +115,7 @@ fun WearApp() {
|
||||
SwipeDismissableNavHost(navController = navController, startDestination = "speaker") {
|
||||
composable("speaker") {
|
||||
StartRecordingScreen(
|
||||
context = context,
|
||||
appState = mainState.appState,
|
||||
isPermissionDenied = mainState.isPermissionDenied,
|
||||
onMicClicked = {
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.birdsounds.identify.presentation
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresPermission
|
||||
@@ -9,13 +10,24 @@ import androidx.compose.foundation.MutatorMutex
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.android.gms.tasks.Tasks
|
||||
import com.google.android.gms.wearable.ChannelClient
|
||||
import com.google.android.gms.wearable.ChannelClient.ChannelCallback
|
||||
import com.google.android.gms.wearable.Wearable
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.tasks.await
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.time.Duration
|
||||
import java.time.LocalDateTime
|
||||
import java.util.concurrent.ExecutionException
|
||||
|
||||
|
||||
class MainState(private val activity: Activity, private val requestPermission: () -> Unit) {
|
||||
private val playbackStateMutatorMutex = MutatorMutex()
|
||||
@@ -34,7 +46,9 @@ class MainState(private val activity: Activity, private val requestPermission: (
|
||||
(ContextCompat.checkSelfPermission(activity, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) -> {
|
||||
Log.e(TAG, "Permissions granted, continuing to record");
|
||||
appState = AppState.Recording
|
||||
record(soundRecorder = soundRecorder, setProgress = { progress -> recordingProgress = progress })
|
||||
|
||||
|
||||
record(activity = activity, soundRecorder = soundRecorder, setProgress = { progress -> recordingProgress = progress })
|
||||
}
|
||||
|
||||
else -> {
|
||||
@@ -55,7 +69,8 @@ class MainState(private val activity: Activity, private val requestPermission: (
|
||||
playbackStateMutatorMutex.mutate {
|
||||
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
|
||||
appState = AppState.Recording
|
||||
record(soundRecorder = soundRecorder, setProgress = { progress ->
|
||||
|
||||
record(activity=activity, soundRecorder = soundRecorder, setProgress = { progress ->
|
||||
recordingProgress = progress
|
||||
})
|
||||
appState = AppState.Ready
|
||||
@@ -73,22 +88,40 @@ sealed class AppState {
|
||||
}
|
||||
|
||||
@RequiresPermission(Manifest.permission.RECORD_AUDIO)
|
||||
private suspend fun record(soundRecorder: SoundRecorder,
|
||||
private suspend fun record(activity: (Activity),soundRecorder: SoundRecorder,
|
||||
setProgress: (progress: Float) -> Unit,
|
||||
maxRecordingDuration: Duration = Duration.ofSeconds(10),
|
||||
numberTicks: Int = 10) {
|
||||
|
||||
val messagePath = "/message_path"
|
||||
|
||||
coroutineScope { // Kick off a parallel job to
|
||||
|
||||
Log.e(TAG, "Mock recording"); // val recordingJob = launch { soundRecorder.record() }
|
||||
val ByteFlow: Flow<String> = flow{
|
||||
for (i in 1..3) {
|
||||
var string_send = LocalDateTime.now().toString()
|
||||
emit(string_send);
|
||||
delay(250);
|
||||
Log.e(TAG, "Emitting " + string_send)
|
||||
}
|
||||
}
|
||||
Log.e(TAG, "Start recording"); //
|
||||
val recordingJob = launch { soundRecorder.record() }
|
||||
// val recordingJob = launch { soundRecorder.record() }
|
||||
// SoundRecorder.record();
|
||||
// val ByteFlow: Flow<String> = flow {
|
||||
// while (true) {
|
||||
// var string_send = LocalDateTime.now().toString()
|
||||
// emit(string_send);
|
||||
// delay(250);
|
||||
// Log.e(TAG, "Emitting " + string_send)
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
//
|
||||
// val c_channel = channelClient.openChannel("Main","Audio");
|
||||
// val channel = c_channel.await();
|
||||
// val OutputStream = channelClient.getOutputStream(channel).await();
|
||||
// OutputStream.write(4);
|
||||
|
||||
// ByteFlow.collect {
|
||||
//
|
||||
//
|
||||
//
|
||||
// Log.e(TAG, "Received " + it)};
|
||||
|
||||
//
|
||||
// val delayPerTickMs = maxRecordingDuration.toMillis() / numberTicks
|
||||
@@ -105,3 +138,9 @@ private suspend fun record(soundRecorder: SoundRecorder,
|
||||
}
|
||||
}
|
||||
|
||||
object channelCallback : ChannelClient.ChannelCallback() {
|
||||
override fun onChannelOpened(channel: ChannelClient.Channel) {
|
||||
super.onChannelOpened(channel)
|
||||
Log.e(TAG,"Opened channel")}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.birdsounds.identify.presentation
|
||||
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.google.android.gms.tasks.Tasks
|
||||
import com.google.android.gms.wearable.Wearable
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.concurrent.ExecutionException
|
||||
|
||||
object MessageSender {
|
||||
const val tag = "MessageSender"
|
||||
private val job = Job()
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.IO + job)
|
||||
|
||||
fun sendMessage(path: String, message: ByteArray, context: Context) {
|
||||
coroutineScope.launch {
|
||||
sendMessageInBackground(path, message, context)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendMessageInBackground(path: String, message: ByteArray, context: Context) {
|
||||
//first get all the nodes, ie connected wearable devices.
|
||||
val nodeListTask = Wearable.getNodeClient(context).connectedNodes
|
||||
try {
|
||||
// Block on a task and get the result synchronously (because this is on a background
|
||||
// thread).
|
||||
val nodes = Tasks.await(nodeListTask)
|
||||
if(nodes.isEmpty()) {
|
||||
Log.i(tag,"No Node found to send message")
|
||||
}
|
||||
//Now send the message to each device.
|
||||
for (node in nodes) {
|
||||
val sendMessageTask = Wearable.getMessageClient(context)
|
||||
.sendMessage(node.id, path, message)
|
||||
try {
|
||||
// Block on a task and get the result synchronously (because this is on a background
|
||||
// thread).
|
||||
val result = Tasks.await(sendMessageTask)
|
||||
Log.v(tag, "SendThread: message send to " + node.displayName)
|
||||
} catch (exception: ExecutionException) {
|
||||
Log.e(tag, "Task failed: $exception")
|
||||
} catch (exception: InterruptedException) {
|
||||
Log.e(tag, "Interrupt occurred: $exception")
|
||||
}
|
||||
}
|
||||
} catch (exception: ExecutionException) {
|
||||
Log.e(tag, "Task failed: $exception")
|
||||
} catch (exception: InterruptedException) {
|
||||
Log.e(
|
||||
tag, "Interrupt occurred: $exception"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,27 +3,24 @@ package com.birdsounds.identify.presentation
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.media.AudioFormat
|
||||
import android.media.AudioRecord
|
||||
import android.media.MediaRecorder
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresPermission
|
||||
import com.google.android.gms.wearable.Wearable
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import java.io.File
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import java.time.LocalDateTime
|
||||
|
||||
/**
|
||||
* A helper class to provide methods to record audio input from the MIC to the internal storage.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
class SoundRecorder(
|
||||
context: Context,
|
||||
context_in: Context,
|
||||
outputFileName: String
|
||||
) {
|
||||
private val audioFile = File(context.filesDir, outputFileName)
|
||||
|
||||
private var state = State.IDLE
|
||||
private var context = context_in
|
||||
|
||||
private enum class State {
|
||||
IDLE, RECORDING
|
||||
@@ -31,39 +28,49 @@ class SoundRecorder(
|
||||
|
||||
|
||||
|
||||
|
||||
@RequiresPermission(Manifest.permission.RECORD_AUDIO)
|
||||
suspend fun record() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
suspendCancellableCoroutine<Unit> { cont ->
|
||||
@Suppress("DEPRECATION")
|
||||
val audioSource = MediaRecorder.AudioSource.DEFAULT
|
||||
val sampleRateInHz = 48000
|
||||
val channelConfig = AudioFormat.CHANNEL_IN_MONO
|
||||
val audioFormat = AudioFormat.ENCODING_PCM_8BIT
|
||||
val bufferSizeInBytes =
|
||||
sampleRateInHz * 1 * 1; // 3 second sample, 2 bytes for each sample
|
||||
val audio_bytes_array = ByteArray(bufferSizeInBytes)
|
||||
val audioRecord = AudioRecord(
|
||||
/* audioSource = */ audioSource,
|
||||
/* sampleRateInHz = */ sampleRateInHz,
|
||||
/* channelConfig = */ channelConfig,
|
||||
/* audioFormat = */ audioFormat,
|
||||
/* bufferSizeInBytes = */ bufferSizeInBytes
|
||||
)
|
||||
|
||||
val mediaRecorder = MediaRecorder().apply {
|
||||
setAudioSource(MediaRecorder.AudioSource.MIC)
|
||||
setOutputFormat(MediaRecorder.OutputFormat.OGG)
|
||||
setAudioEncoder(MediaRecorder.AudioEncoder.OPUS)
|
||||
setOutputFile(audioFile.path)
|
||||
setOnInfoListener { mr, what, extra ->
|
||||
println("info: $mr $what $extra")
|
||||
}
|
||||
setOnErrorListener { mr, what, extra ->
|
||||
println("error: $mr $what $extra")
|
||||
}
|
||||
}
|
||||
|
||||
cont.invokeOnCancellation {
|
||||
mediaRecorder.stop()
|
||||
state = State.IDLE
|
||||
}
|
||||
|
||||
mediaRecorder.prepare()
|
||||
mediaRecorder.start()
|
||||
Log.e("com.birdsounds.identify","Hey I'm recording")
|
||||
state = State.RECORDING
|
||||
val thread = Thread {
|
||||
while (true) {
|
||||
val out = audioRecord.read(
|
||||
/* audioData = */ audio_bytes_array,
|
||||
/* offsetInBytes = */ 0,
|
||||
/* sizeInBytes = */ bufferSizeInBytes,
|
||||
/* readMode = */ AudioRecord.READ_BLOCKING
|
||||
)
|
||||
|
||||
|
||||
// val audio_u_byte = audio_bytes_array.toUByteArray();
|
||||
// Log.w(TAG, audio_bytes_array.size.toString());
|
||||
val str_beg = audio_bytes_array[0].toString()
|
||||
val str_end = audio_bytes_array[bufferSizeInBytes-1].toString()
|
||||
Log.w(TAG, str_beg + ", " + str_end);
|
||||
// MessageSender.sendMessage("/audio",audio_bytes_array, context)
|
||||
|
||||
}
|
||||
};
|
||||
thread.start();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.birdsounds.identify.presentation
|
||||
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
@@ -10,6 +11,7 @@ import com.google.android.horologist.compose.layout.ScreenScaffold
|
||||
|
||||
@Composable
|
||||
fun StartRecordingScreen(
|
||||
context: Context,
|
||||
appState: AppState,
|
||||
isPermissionDenied: Boolean,
|
||||
onMicClicked: () -> Unit
|
||||
@@ -53,16 +55,3 @@ private class PlaybackStatePreviewProvider : CollectionPreviewParameterProvider<
|
||||
AppState.Ready
|
||||
)
|
||||
)
|
||||
|
||||
@WearPreviewDevices
|
||||
@WearPreviewFontScales
|
||||
@Composable
|
||||
fun SpeakerScreenPreview(
|
||||
@PreviewParameter(PlaybackStatePreviewProvider::class) appState: AppState
|
||||
) {
|
||||
StartRecordingScreen(
|
||||
appState = appState,
|
||||
isPermissionDenied = true,
|
||||
onMicClicked = {}
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user