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 - -[Get it on F-Droid](https://f-droid.org/packages/org.noise_planet.noisecapture/) -[Get it on Google Play](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"