From 59dbca47a5142cb87b0c022d08d9cb6296737921 Mon Sep 17 00:00:00 2001
From: nicolas-f <1382241+nicolas-f@users.noreply.github.com>
Date: Thu, 7 Mar 2024 17:19:52 +0100
Subject: [PATCH 1/3] legend will be drawn in canvas
---
.github/workflows/static.yml | 2 +-
README.md | 34 +++----------------
.../shared/child/MeasurementScreen.kt | 26 +++++++++++++-
3 files changed, 31 insertions(+), 31 deletions(-)
diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml
index 8bc2843..767426d 100644
--- a/.github/workflows/static.yml
+++ b/.github/workflows/static.yml
@@ -48,7 +48,7 @@ jobs:
with:
key: noisecapturejs
path: |
- webApp/build
+ $GITHUB_WORKSPACE/webApp/build
- name: Build
run: >
./gradlew
diff --git a/README.md b/README.md
index b0ca036..52a209b 100644
--- a/README.md
+++ b/README.md
@@ -1,36 +1,12 @@
-# About NoiseCapture App
+# About NoiseCapture
-![Build status](https://github.com/nicolas-f/NoiseCaptureKotlin/actions/workflows/test.yml/badge.svg)
+![Build status](https://github.com/nicolas-f/NoiseCaptureKotlin/actions/workflows/code_review.yml/badge.svg)
-**NoiseCapture App** is Android App dedicated to the measurement of environmental noise.
+**NoiseCapture** is an Android/Ios/Web App dedicated to the measurement of environmental noise.
## Description
-**NoiseCapture App** is an Android App project for measuring environmental noise using a smartphone. The goal is to **produce relevant noise indicators from audio measurements, including a geospatial representation**. Measurements can be shared with the community in order to produce participatory noise maps. **NoiseCapture App** is a component of a global infrastructure, _i.e._ a Spatial Data Infrastructure (SDI), called the **OnoMap SDI**, that allows to process and represent the geospatial information, like noise maps.
-
-* A [**full description**](https://github.com/Ifsttar/NoiseCapture/wiki) of the whole OnoMap SDI, including the NoiseCapture App, is given in the [wiki pages](https://github.com/Ifsttar/NoiseCapture/wiki).
-* An **user guide**, for the use of the NoiseCapture App, is proposed within the NoiseCapture App (see the 'Help' page in the menu of NoiseCapture App).
-
-## Features
-
-NoiseCapture App features are divided into 3 parts:
-
- - Measurement - Once the sound level calibration is done, the user start the measurement in order to record each second the LAeq, an average sound energy over a period of 1s. The spectrum repartition of the sound are analysed and stored using the Fourrier transform. The device location are recorded while measuring the sound level. The user has the hability to provide his own feedback about the feeling of the noise environment.
-
- - Extented report - Advanced statistics are computed locally on the phone and shown to the user. For each user's measurement the locations of the noise levels are displayed in a map.
-
- - Share results with the community - Anonymous results are transfered to Virtual Hubs (web server) and post-processed in order to build a noise map that merge all community results. Participative noise maps can be displayed within the NoiseCapture App, or online at https://onomap.noise-planet.org/.
-
-## Developments
-NoiseCapture App is a collaboration between the [Environmental Acoustic Research unit](http://www.umrae.fr/en/) ([Ifsttar](http://www.ifsttar.fr)) and the [Lab-STICC](http://www.lab-sticc.fr/) CNRS. If you need more information about the project developped by the Environmental Acoustic Research unit and the Lab-STICC, on this topic, go to [http://www.noise-planet.org](http://noise-planet.org).
-
-## Download
-
-[](https://f-droid.org/packages/org.noise_planet.noisecapture/)
-[](https://play.google.com/store/apps/details?id=org.noise_planet.noisecapture)
+**NoiseCapture** is an Android/Ios/Web App for measuring environmental noise using a smartphone/tablet device.
+The goal is to **produce relevant noise indicators from audio measurements, including a geospatial representation**. Measurements can be shared with the community in order to produce participatory noise maps. **NoiseCapture App** is a component of a global infrastructure, _i.e._ a Spatial Data Infrastructure (SDI), called the **OnoMap SDI**, that allows to process and represent the geospatial information, like noise maps.
## Funding
This application was developed under the initial funding the European project [ENERGIC-OD](http://www.energic-od.eu/), with the help of the [GEOPAL](http://www.geopal.org/accueil) program.
diff --git a/shared/src/commonMain/kotlin/org/noise_planet/noisecapture/shared/child/MeasurementScreen.kt b/shared/src/commonMain/kotlin/org/noise_planet/noisecapture/shared/child/MeasurementScreen.kt
index e5b886d..b8fb151 100644
--- a/shared/src/commonMain/kotlin/org/noise_planet/noisecapture/shared/child/MeasurementScreen.kt
+++ b/shared/src/commonMain/kotlin/org/noise_planet/noisecapture/shared/child/MeasurementScreen.kt
@@ -2,8 +2,12 @@ package org.noise_planet.noisecapture.shared.child
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.requiredWidth
+import androidx.compose.foundation.layout.width
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
@@ -15,8 +19,13 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.drawText
+import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
import com.bumble.appyx.components.backstack.BackStack
import com.bumble.appyx.navigation.modality.BuildContext
import com.bumble.appyx.navigation.node.Node
@@ -43,6 +52,9 @@ class MeasurementScreen(buildContext: BuildContext, val backStack: BackStack 1000) {
+ if(frequency%1000 > 0) {
+ val subKilo = (frequency%1000).toString().trimEnd('0')
+ "${frequency/1000}.$subKilo kHz"
+ } else {
+ "${frequency/1000} kHz"
+ }
+ } else {
+ "$frequency Hz"
+ }
+ }
+
@Composable
override fun View(modifier: Modifier) {
var noiseLevel by remember { mutableStateOf(0.0) }
@@ -123,7 +148,6 @@ class MeasurementScreen(buildContext: BuildContext, val backStack: BackStack
Date: Tue, 12 Mar 2024 15:38:52 +0100
Subject: [PATCH 2/3] y label ok
---
.../shared/child/MeasurementScreen.kt | 109 +++++++++++++++---
.../shared/ui/SpectrogramBitmap.kt | 12 +-
2 files changed, 97 insertions(+), 24 deletions(-)
diff --git a/shared/src/commonMain/kotlin/org/noise_planet/noisecapture/shared/child/MeasurementScreen.kt b/shared/src/commonMain/kotlin/org/noise_planet/noisecapture/shared/child/MeasurementScreen.kt
index b8fb151..7b34709 100644
--- a/shared/src/commonMain/kotlin/org/noise_planet/noisecapture/shared/child/MeasurementScreen.kt
+++ b/shared/src/commonMain/kotlin/org/noise_planet/noisecapture/shared/child/MeasurementScreen.kt
@@ -12,6 +12,8 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -21,12 +23,19 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.drawText
import androidx.compose.ui.text.rememberTextMeasurer
+import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import com.bumble.appyx.components.backstack.BackStack
+import com.bumble.appyx.navigation.lifecycle.DefaultPlatformLifecycleObserver
+import com.bumble.appyx.navigation.lifecycle.Lifecycle
import com.bumble.appyx.navigation.modality.BuildContext
import com.bumble.appyx.navigation.node.Node
import kotlinx.coroutines.launch
@@ -36,7 +45,11 @@ import org.noise_planet.noisecapture.shared.MeasurementService
import org.noise_planet.noisecapture.shared.ScreenData
import org.noise_planet.noisecapture.shared.ui.SpectrogramBitmap
import org.noise_planet.noisecapture.toImageBitmap
+import kotlin.math.log
+import kotlin.math.log10
+import kotlin.math.max
import kotlin.math.min
+import kotlin.math.pow
import kotlin.math.round
const val FFT_SIZE = 4096
@@ -53,23 +66,79 @@ class MeasurementScreen(buildContext: BuildContext, val backStack: BackStack SpectrogramBitmap.frequencyLegendPositionLog
+ else -> SpectrogramBitmap.frequencyLegendPositionLinear
+ }
+ val maxYLabelWidth =
+ frequencyLegendPosition.maxOf { frequency ->
+ val text = formatFrequency(frequency)
+ textMeasurer.measure(text).size.width
+ }
+ Canvas(modifier = Modifier.fillMaxSize() ) {
+ drawRect(color = Color.Black, size=size)
val canvasSize = IntSize(SPECTROGRAM_STRIP_WIDTH, size.height.toInt())
- spectrumCanvasState.spectrogramCanvasSize = size
+ val tickLength = 4.dp.toPx()
+ val legendWidth = maxYLabelWidth+tickLength
+ spectrumCanvasState.spectrogramCanvasSize = Size(size.width - legendWidth, size.height)
if(spectrumCanvasState.currentStripData.size != canvasSize) {
// reset buffer on resize or first draw
- spectrumCanvasState.currentStripData = SpectrogramBitmap.createSpectrogram(canvasSize)
+ spectrumCanvasState.currentStripData = SpectrogramBitmap.createSpectrogram(
+ canvasSize, SpectrogramBitmap.Companion.SCALE_MODE.SCALE_LOG, spectrumCanvasState.currentStripData.sampleRate)
spectrumCanvasState.cachedStrips.clear()
} else {
- drawImage(spectrumCanvasState.currentStripData.byteArray.toImageBitmap(),
- topLeft = Offset(size.width - spectrumCanvasState.currentStripData.offset, 0F))
- spectrumCanvasState.cachedStrips.reversed().forEachIndexed { index, imageBitmap ->
- val bitmapX = size.width - ((index + 1) * SPECTROGRAM_STRIP_WIDTH
- + spectrumCanvasState.currentStripData.offset).toFloat()
- drawImage(imageBitmap,
- topLeft = Offset(bitmapX, 0F))
+ if(spectrumCanvasState.currentStripData.sampleRate > 1) {
+ drawImage(
+ spectrumCanvasState.currentStripData.byteArray.toImageBitmap(),
+ topLeft = Offset(
+ size.width - spectrumCanvasState.currentStripData.offset,
+ 0F
+ )
+ )
+ spectrumCanvasState.cachedStrips.reversed()
+ .forEachIndexed { index, imageBitmap ->
+ val bitmapX = size.width - ((index + 1) * SPECTROGRAM_STRIP_WIDTH
+ + spectrumCanvasState.currentStripData.offset).toFloat()
+ drawImage(
+ imageBitmap,
+ topLeft = Offset(bitmapX, 0F)
+ )
+ }
+ drawRect(color = Color.Black, size = Size(legendWidth, size.height))
+ // draw legend
+ val fMax = spectrumCanvasState.currentStripData.sampleRate / 2
+ val fMin = frequencyLegendPosition[0].toDouble()
+ val r = fMax / fMin
+ val sheight = spectrumCanvasState.currentStripData.size.height
+ frequencyLegendPosition.forEachIndexed { index, frequency ->
+ val text = buildAnnotatedString {
+ withStyle(style = SpanStyle(color = Color.White)) {
+ append(formatFrequency(frequency))
+ }
+ }
+ val textSize = textMeasurer.measure(text)
+ val tickHeightPos = when (spectrumCanvasState.currentStripData.scaleMode) {
+ SpectrogramBitmap.Companion.SCALE_MODE.SCALE_LOG -> {
+ sheight - (log10(frequency / fMin) / ((log10(r) / sheight))).toInt()
+ }
+
+ else -> 0
+ }
+ drawLine(
+ color = Color.White, start = Offset(
+ legendWidth-tickLength,
+ tickHeightPos.toFloat()
+ ),
+ end = Offset(
+ legendWidth,
+ tickHeightPos.toFloat()
+ ),
+ strokeWidth = 2.dp.toPx()
+ )
+ val textPos = min(sheight - textSize.size.height,
+ max(0, tickHeightPos - textSize.size.height / 2))
+ drawText(textMeasurer, text, topLeft = Offset(legendWidth-textSize.size.width-tickLength, textPos.toFloat()))
+ }
}
}
}
@@ -94,7 +163,7 @@ class MeasurementScreen(buildContext: BuildContext, val backStack: BackStack spectrumCanvasState.spectrogramCanvasSize.width) {
+ if((spectrumCanvasState.cachedStrips.size - 1) *
+ SPECTROGRAM_STRIP_WIDTH >
+ spectrumCanvasState.spectrogramCanvasSize.width) {
// remove offscreen bitmaps
spectrumCanvasState.cachedStrips.removeAt(0)
}
- spectrumCanvasState.currentStripData = SpectrogramBitmap.createSpectrogram(spectrumCanvasState.currentStripData.size)
+ spectrumCanvasState.currentStripData =
+ SpectrogramBitmap.createSpectrogram(
+ spectrumCanvasState.currentStripData.size,
+ spectrumCanvasState.currentStripData.scaleMode,
+ measurementService!!.sampleRate.toDouble())
bitmapChanged = false
continue
}
spectrumCanvasState.currentStripData.pushSpectrumToSpectrogramData(
measurementServiceData.spectrumDataList.subList(indexToProcess,
indexToProcess + subListSizeToCompleteStrip),
- SpectrogramBitmap.Companion.SCALE_MODE.SCALE_LOG,
- mindB, rangedB, measurementService!!.sampleRate.toDouble()
- )
+ mindB, rangedB)
bitmapChanged = true
indexToProcess += subListSizeToCompleteStrip
}
diff --git a/shared/src/commonMain/kotlin/org/noise_planet/noisecapture/shared/ui/SpectrogramBitmap.kt b/shared/src/commonMain/kotlin/org/noise_planet/noisecapture/shared/ui/SpectrogramBitmap.kt
index baefddf..594b20e 100644
--- a/shared/src/commonMain/kotlin/org/noise_planet/noisecapture/shared/ui/SpectrogramBitmap.kt
+++ b/shared/src/commonMain/kotlin/org/noise_planet/noisecapture/shared/ui/SpectrogramBitmap.kt
@@ -102,7 +102,7 @@ class SpectrogramBitmap {
"#F75500".toComposeColor(),
"#FB2A00".toComposeColor(),
)
- fun createSpectrogram(size: IntSize) : SpectrogramDataModel {
+ fun createSpectrogram(size: IntSize, scaleMode: SCALE_MODE, sampleRate: Double) : SpectrogramDataModel {
val byteArray = ByteArray(bmpHeader.size + Int.SIZE_BYTES * size.width * size.height)
bmpHeader.copyInto(byteArray)
// fill with changing header data
@@ -111,7 +111,7 @@ class SpectrogramBitmap {
(rawPixelSize+bmpHeader.size).toLittleEndianBytes().copyInto(byteArray, sizeIndex)
size.width.toLittleEndianBytes().copyInto(byteArray, widthIndex)
size.height.toLittleEndianBytes().copyInto(byteArray, heightIndex)
- return SpectrogramDataModel(size, byteArray)
+ return SpectrogramDataModel(size, byteArray, scaleMode = scaleMode, sampleRate = sampleRate)
}
/**
@@ -126,7 +126,9 @@ class SpectrogramBitmap {
* @constructor
* @si
*/
- data class SpectrogramDataModel(val size: IntSize, val byteArray: ByteArray, var offset : Int = 0) {
+ data class SpectrogramDataModel(val size: IntSize, val byteArray: ByteArray,
+ var offset : Int = 0, val scaleMode: SCALE_MODE,
+ val sampleRate: Double) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
@@ -144,9 +146,7 @@ class SpectrogramBitmap {
}
fun pushSpectrumToSpectrogramData(fftResults : List,
- scaleMode: SCALE_MODE,
- mindB : Double, rangedB : Double,
- sampleRate: Double) {
+ mindB : Double, rangedB : Double) {
// generate columns of pixels
// merge power of each frequencies following the destination bitmap resolution
val hertzBySpectrumCell = sampleRate / FFT_SIZE.toDouble()
From 06de63e69ec9c936abb618f837ac0ef4f2f72e1b Mon Sep 17 00:00:00 2001
From: nicolas-f <1382241+nicolas-f@users.noreply.github.com>
Date: Tue, 12 Mar 2024 17:45:55 +0100
Subject: [PATCH 3/3] x axis ok
---
.../shared/child/MeasurementScreen.kt | 65 +++++++++++++++----
1 file changed, 51 insertions(+), 14 deletions(-)
diff --git a/shared/src/commonMain/kotlin/org/noise_planet/noisecapture/shared/child/MeasurementScreen.kt b/shared/src/commonMain/kotlin/org/noise_planet/noisecapture/shared/child/MeasurementScreen.kt
index 7b34709..d8eda27 100644
--- a/shared/src/commonMain/kotlin/org/noise_planet/noisecapture/shared/child/MeasurementScreen.kt
+++ b/shared/src/commonMain/kotlin/org/noise_planet/noisecapture/shared/child/MeasurementScreen.kt
@@ -45,6 +45,8 @@ import org.noise_planet.noisecapture.shared.MeasurementService
import org.noise_planet.noisecapture.shared.ScreenData
import org.noise_planet.noisecapture.shared.ui.SpectrogramBitmap
import org.noise_planet.noisecapture.toImageBitmap
+import kotlin.math.ceil
+import kotlin.math.floor
import kotlin.math.log
import kotlin.math.log10
import kotlin.math.max
@@ -70,6 +72,8 @@ class MeasurementScreen(buildContext: BuildContext, val backStack: BackStack SpectrogramBitmap.frequencyLegendPositionLog
else -> SpectrogramBitmap.frequencyLegendPositionLinear
}
+ val timeXLabelMeasure = textMeasurer.measure(" +99s ")
+ val timeXLabelHeight = timeXLabelMeasure.size.height
val maxYLabelWidth =
frequencyLegendPosition.maxOf { frequency ->
val text = formatFrequency(frequency)
@@ -77,10 +81,13 @@ class MeasurementScreen(buildContext: BuildContext, val backStack: BackStack
- val bitmapX = size.width - ((index + 1) * SPECTROGRAM_STRIP_WIDTH
+ val bitmapX = size.width - legendWidth - ((index + 1) * SPECTROGRAM_STRIP_WIDTH
+ spectrumCanvasState.currentStripData.offset).toFloat()
drawImage(
imageBitmap,
topLeft = Offset(bitmapX, 0F)
)
}
- drawRect(color = Color.Black, size = Size(legendWidth, size.height))
- // draw legend
+ // black background of legend
+ drawRect(color = Color.Black, size = Size(legendWidth, size.height),
+ topLeft = Offset(size.width - legendWidth, 0F))
+ // draw Y axe labels
val fMax = spectrumCanvasState.currentStripData.sampleRate / 2
val fMin = frequencyLegendPosition[0].toDouble()
val r = fMax / fMin
@@ -126,18 +135,46 @@ class MeasurementScreen(buildContext: BuildContext, val backStack: BackStack
+ val timeValue = (1 + timeBetweenLabels * (labelIndex - 1)).toInt()
+ val xPos = (xLegendWidth - timeValue / timePerPixel).toFloat()
+ drawLine(
+ color = Color.White, start = Offset(
+ xPos-tickStroke.toPx()/2,
+ sheight.toFloat()
+ ),
+ end = Offset(
+ xPos-tickStroke.toPx()/2,
+ sheight.toFloat() + tickLength
+ ),
+ strokeWidth = tickStroke.toPx()
+ )
+ val legendText = buildAnnotatedString {
+ withStyle(style = SpanStyle(color = Color.White)) {
+ append("+${timeValue}s")
+ }
+ }
+ drawText(textMeasurer,legendText, topLeft = Offset(xPos-textMeasurer.measure(legendText).size.width / 2, sheight.toFloat() + tickLength))
}
}
}
@@ -146,7 +183,7 @@ class MeasurementScreen(buildContext: BuildContext, val backStack: BackStack 1000) {
+ return if (frequency >= 1000) {
if(frequency%1000 > 0) {
val subKilo = (frequency%1000).toString().trimEnd('0')
"${frequency/1000}.$subKilo kHz"