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


    Developer (m/w/d) Backend/ Mobile

    Payback GmbH
    Cologne, Germany
    • Full Time
    apply now

    Android Engineer

    American Express
    Phoenix, USA
    • Full Time
    apply now

    Engineering Manager – Apps Lifecycle

    Zalando SE
    Berlin
    • Full Time
    apply now
Load more listings

OUR VIDEO RECOMMENDATION

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

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE
blog
Yes! You heard it right. We’ll try to understand the complete OTP (one time…
READ MORE

Leave a Reply

Your email address will not be published.

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

Menu