Blog Infos
Author
Published
Topics
,
Published
Practical example of the power of Jetpack Compose

 

Chromatic tuner is for detecting the pitch of 12 musical notes, allowing instruments to be tuned correctly.

All musical notes have their specific vibration frequency. Note A4, for example, oscillates at 440Hz. Based on this, our goal is to develop an application that detects the frequency being played and how above or below the specific frequency of each of the 12 notes, so that the musician can tune his instrument correctly. Let’s go?

User Interface

Our app’s interface will be quite simple:

user interface image, demonstrating my strong design skills… 😅
Setting up Project

It doesn’t take much mystery to create a project that supports Wear OS and Compose. You can create a new project as usual and add these three tags to AndroidManifest.xml:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="dev.arildo.tuner">
<uses-feature android:name="android.hardware.type.watch" />
<application
android:allowBackup="true".........>
<uses-library android:name="com.google.android.wearable" android:required="true" />
<meta-data android:name="com.google.android.wearable.standalone" android:value="true" />
Wear Compose

In addition to the conventional Compose packages, there are also specific packages for wearable devices:

implementation "androidx.wear.compose:compose-foundation:$version"
implementation "androidx.wear.compose:compose-material:$version"
implementation "androidx.wear.compose:compose-navigation:$version"

https://developer.android.com/jetpack/androidx/releases/wear-compose

 

dependencies {
implementation 'androidx.core:core-ktx:1.7.0'
// Compose
implementation "androidx.activity:activity-compose:1.3.1"
implementation "androidx.compose.runtime:runtime-livedata:1.0.5"
implementation "androidx.compose.ui:ui-tooling-preview:1.0.5"
implementation "androidx.compose.ui:ui:1.0.5"
implementation "androidx.compose.compiler:compiler:1.0.5"
implementation "androidx.compose.foundation:foundation:1.0.5"
implementation "androidx.wear.compose:compose-material:1.0.0-alpha10"
implementation "androidx.wear.compose:compose-foundation:1.0.0-alpha10"
debugImplementation "androidx.compose.ui:ui-tooling:1.0.5"
// TarsosDSP
implementation files('../libs/tarsos-dsp-android.jar')
}

Okay, dependencies added, now the project is ready to start development. This is our MainActivity():

class MainActivity : ComponentActivity() {
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme { }
}
}
}
Note that we extend from ComponentActivity() and not more from AppCompatActivity()
MaterialTheme {
Scaffold(
timeText = { TimeText() }
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxSize()
) {
Text("Hello World", color = Color.White)
}
}
}

preview image of our hello world in the emulator

 

@Composable
fun TunerScreen(tunerState: TunerState) {
Column(
modifier = Modifier.fillMaxSize().background(tunerState.bgColor),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(verticalAlignment = Alignment.CenterVertically) {
LeftArrow(isVisible = tunerState is TunerState.Down)
TextNote(note = tunerState.note.title)
RightArrow(isVisible = tunerState is TunerState.Up)
}
}
}
@Preview(widthDp = 200, heightDp = 200)
@Composable
fun TunedView() = TunerScreen(TunerState.Tuned(NotesEnum.A))
@Preview(widthDp = 200, heightDp = 200)
@Composable
fun OutOfTuneViewDown() = TunerScreen(TunerState.Down(NotesEnum.A))
@Preview(widthDp = 200, heightDp = 200)
@Composable
fun OutOfTuneViewUp() = TunerScreen(TunerState.Up(NotesEnum.A))
view raw TunerScreen.kt hosted with ❤ by GitHub

Once that’s done, we’ll have the following previews:

 

preview image of the three screen states

 

Note that in the code above, the TunerScreen() function receives a TunerState(), which is a sealed class I created to make it easier to manage the states:

sealed class TunerState(open val note: NotesEnum, val bgColor: Color) {
class Down(override val note: NotesEnum) : TunerState(note, OutOfTuneColor)
class Tuned(override val note: NotesEnum) : TunerState(note, TunedColor)
class Up(override val note: NotesEnum) : TunerState(note, OutOfTuneColor)
}
view raw TunerState.kt hosted with ❤ by GitHub
Frequency Detection

The TarsosDSP library has the PitchDetectionHandler interface that, among several resources, provides us with the frequency and volume of the sound being captured. With that, just use these values obtained and implement the logic of comparison with the reference values we have for each of the 12 notes and their octaves:

Source: https://www.treinaweb.com.br/blog/gerando-sons-com-a-web-audio-api-do-javascript

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

Creating your first Wear OS app using compose

This will be an introductory session into the world of wearable apps. In this session we’ll discuss how to create a latest up to date wearable app by using “Compose for Wear OS” and Horologist…
Watch Video

Creating your first Wear OS app using compose

Mikhail Kulaha
Compose engineer
Google

Creating your first Wear OS app using compose

Mikhail Kulaha
Compose engineer
Google

Creating your first Wear OS app using compose

Mikhail Kulaha
Compose engineer
Google

Jobs

PitchDetectionHandler { result, audioEvent ->
val pitchInHz = result.pitch.toDouble()
if (shouldUpdateTunerState(pitchInHz, audioEvent)) {
val capturedNoteState = getCurrentPitchState(pitchInHz)
_tunerState.postValue(capturedNoteState)
saveLastUpdatedTime()
}
}
fun getCurrentPitchState(pitchHz: Double): TunerState {
val closestFrequency = getClosestFrequencyInAllNotes(pitchHz)
val note = getNoteByFrequency(closestFrequency)
val diff = if (closestFrequency > pitchHz) {
abs(closestFrequency - pitchHz).unaryMinus()
} else {
abs(pitchHz - closestFrequency)
}
return when {
diff.isInPermittedTolerance() -> TunerState.Tuned(note)
diff < -0.5 -> TunerState.Down(note)
else -> TunerState.Up(note)
}
}
  • Get the note corresponding to the closest frequency;
  • Calculate the difference between the input frequency and the reference frequency;
  • Return an instance of TunerState(), taking into account this difference, whether it is within acceptable limits, below or above.
fun Double.isInPermittedTolerance() = this in -0.5..0.5
LiveData.observeAsState()

Another thing worth talking about is how our ui observes the information propagated to LiveData. Compose has the extension LiveData.observeAsState(), which works very similarly to the traditional way of observing changes in LiveData, with the Observer interface. The difference is that using this approach, the screen is automatically recomposed and the code is much cleaner:

Scaffold(
timeText = { TimeText() }
) {
TunerScreen(viewModel.tunerState.observeAsState().value)
}
Action

To validate how our tuner works, we’re going to use a tone generator app reproducing specific frequencies in order to verify that the tuner is behaving as expected. As we know that the A4 note has a frequency of 440Hz, let’s use it in our test cases:

  • Frequency 440.5Hz should display Note A in tune
  • Frequency 439.4Hz should display Note A tuned down
  • Frequency 440.6Hz should display Note A tuned up

 

screenshots of my Galaxy Watch4 + smartphone

 

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE

1 Comment. Leave new

  • Peter Dynow
    08.03.2023 17:23

    Hello Arildo!
    First of all, thank you for the great app!
    I have a question about this: In the orchestra, the preferred pitch for the concert pitch A is not 440 Hertz, but 443 Hertz. Is it possible to customize the app accordingly?
    Thank you and best regards, Peter Dynow

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu