commit e068ad52de0aac0df9d88fe4f525a9669d182961 Author: sergeych Date: Mon Oct 13 11:23:20 2025 +0400 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..071fe3c --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +*.iml +.gradle +/local.properties +/.idea +/.kotlin +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..036f698 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,60 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) +} + +android { + namespace = "net.sergeych.karabass" + compileSdk { + version = release(36) + } + + defaultConfig { + applicationId = "net.sergeych.karabass" + minSdk = 24 + targetSdk = 36 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } + buildFeatures { + compose = true + } +} + +dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.graphics) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.material3) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.compose.ui.test.junit4) + debugImplementation(libs.androidx.compose.ui.tooling) + debugImplementation(libs.androidx.compose.ui.test.manifest) +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/net/sergeych/karabass/ExampleInstrumentedTest.kt b/app/src/androidTest/java/net/sergeych/karabass/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..128dbd3 --- /dev/null +++ b/app/src/androidTest/java/net/sergeych/karabass/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package net.sergeych.karabass + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("net.sergeych.karabass", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..4a484eb --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/net/sergeych/karabass/AdvancedTubaSynth.kt b/app/src/main/java/net/sergeych/karabass/AdvancedTubaSynth.kt new file mode 100644 index 0000000..1709646 --- /dev/null +++ b/app/src/main/java/net/sergeych/karabass/AdvancedTubaSynth.kt @@ -0,0 +1,399 @@ +package net.sergeych.karabass + +import android.content.Context +import android.media.AudioAttributes +import android.media.AudioFormat +import android.media.AudioManager +import android.media.AudioTrack +import android.os.Build +import kotlin.math.* + +class AdvancedTubaSynth(private val context: Context) { + + companion object { + const val SAMPLE_RATE = 44100 + const val CUTOFF_FREQ = 800.0 + + // Полный звуковой ряд тубы с полутонами (от C1 до B5) + val TUBA_NOTES = listOf( + // Октава 1 + "C1" to 32.70f, "C#1" to 34.65f, "D1" to 36.71f, "D#1" to 38.89f, "E1" to 41.20f, "F1" to 43.65f, + "F#1" to 46.25f, "G1" to 49.00f, "G#1" to 51.91f, "A1" to 55.00f, "A#1" to 58.27f, "B1" to 61.74f, + + // Октава 2 + "C2" to 65.41f, "C#2" to 69.30f, "D2" to 73.42f, "D#2" to 77.78f, "E2" to 82.41f, "F2" to 87.31f, + "F#2" to 92.50f, "G2" to 98.00f, "G#2" to 103.83f, "A2" to 110.00f, "A#2" to 116.54f, "B2" to 123.47f, + + // Октава 3 + "C3" to 130.81f, "C#3" to 138.59f, "D3" to 146.83f, "D#3" to 155.56f, "E3" to 164.81f, "F3" to 174.61f, + "F#3" to 185.00f, "G3" to 196.00f, "G#3" to 207.65f, "A3" to 220.00f, "A#3" to 233.08f, "B3" to 246.94f, + + // Октава 4 + "C4" to 261.63f, "C#4" to 277.18f, "D4" to 293.66f, "D#4" to 311.13f, "E4" to 329.63f, "F4" to 349.23f, + "F#4" to 369.99f, "G4" to 392.00f, "G#4" to 415.30f, "A4" to 440.00f, "A#4" to 466.16f, "B4" to 493.88f, + + // Октава 5 (верхний предел для продвинутых исполнителей) + "C5" to 523.25f, "C#5" to 554.37f, "D5" to 587.33f, "D#5" to 622.25f, "E5" to 659.25f, "F5" to 698.46f, + "F#5" to 739.99f, "G5" to 783.99f, "G#5" to 830.61f, "A5" to 880.00f, "A#5" to 932.33f, "B5" to 987.77f + ) + } + + @Volatile private var audioTrack: AudioTrack? = null + @Volatile private var isPlaying = false + @Volatile private var shouldStop = false + private var currentThread: Thread? = null + + // Синхронизированные переменные для потокобезопасности + private val lock = Object() + private var currentFrequency = 0f + private var targetFrequency = 0f + private var isNoteOn = false + private var currentAmplitude = 0.0 + private var samplesSinceNoteOn = 0 + private var samplesSinceNoteOff = 0 + private var phase = 0.0 + + // Явное состояние огибающей + private enum class EnvelopeState { IDLE, ATTACK, DECAY, SUSTAIN, RELEASE } + private var envelopeState = EnvelopeState.IDLE + + // Оптимизированные параметры для уменьшения задержки + private val attackTime = (0.005 * SAMPLE_RATE).toInt() // 5ms атака + private val decayTime = (0.03 * SAMPLE_RATE).toInt() // 30ms спад + private val releaseTime = (0.05 * SAMPLE_RATE).toInt() // 50ms релиз + private val sustainLevel = 0.8 + + fun startNote(frequency: Float) { + synchronized(lock) { + targetFrequency = frequency + + if (!isNoteOn) { + // Новая нота - сбрасываем все счетчики + isNoteOn = true + samplesSinceNoteOn = 0 + samplesSinceNoteOff = 0 + envelopeState = EnvelopeState.ATTACK + currentFrequency = frequency + + if (!isPlaying) { + startAudioGeneration() + } + } else { + // Легато - быстрый переход + targetFrequency = frequency + } + } + } + + fun stopNote() { + synchronized(lock) { + if (isNoteOn) { + isNoteOn = false + samplesSinceNoteOff = 0 + // Переходим в состояние релиза только если мы не в IDLE + if (envelopeState != EnvelopeState.IDLE) { + envelopeState = EnvelopeState.RELEASE + } + } + } + } + + fun changeNote(frequency: Float) { + synchronized(lock) { + targetFrequency = frequency + } + } + + private fun startAudioGeneration() { + shouldStop = false + isPlaying = true + + // Минимальный буфер для low-latency + val bufferSize = 512 // ~11ms latency + + try { + audioTrack = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val attributes = AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_MEDIA) + .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) + .build() + + val format = AudioFormat.Builder() + .setSampleRate(SAMPLE_RATE) + .setEncoding(AudioFormat.ENCODING_PCM_16BIT) + .setChannelMask(AudioFormat.CHANNEL_OUT_MONO) + .build() + + AudioTrack.Builder() + .setAudioAttributes(attributes) + .setAudioFormat(format) + .setBufferSizeInBytes(bufferSize * 2) + .build() + } else { + @Suppress("DEPRECATION") + AudioTrack( + AudioManager.STREAM_MUSIC, + SAMPLE_RATE, + AudioFormat.CHANNEL_OUT_MONO, + AudioFormat.ENCODING_PCM_16BIT, + bufferSize * 2, + AudioTrack.MODE_STREAM + ) + } + + audioTrack?.play() + } catch (e: Exception) { + e.printStackTrace() + isPlaying = false + return + } + + currentThread = Thread { + val buffer = ShortArray(bufferSize) + + try { + while (!shouldStop && isPlaying) { + // Заполняем буфер + for (i in buffer.indices) { + val sample = generateSample() + buffer[i] = (sample.coerceIn(-1.0, 1.0) * Short.MAX_VALUE).toInt().toShort() + } + + // Безопасная запись в AudioTrack + val track = audioTrack + if (track?.playState == AudioTrack.PLAYSTATE_PLAYING) { + try { + track.write(buffer, 0, buffer.size) + } catch (e: Exception) { + // Игнорируем ошибки записи при остановке + if (!shouldStop) { + e.printStackTrace() + } + break + } + } else { + break + } + + // Проверяем, нужно ли остановить генерацию + synchronized(lock) { + if (envelopeState == EnvelopeState.IDLE && !isNoteOn) { + isPlaying = false + } + } + } + } catch (e: Exception) { + e.printStackTrace() + } finally { + // Безопасная остановка + try { + audioTrack?.stop() + } catch (e: Exception) { + e.printStackTrace() + } + isPlaying = false + } + }.apply { + start() + } + } + + private fun generateSample(): Double { + synchronized(lock) { + // Быстрое изменение частоты для легато + if (abs(currentFrequency - targetFrequency) > 0.1) { + currentFrequency += (targetFrequency - currentFrequency) * 0.3f + } + + // Расчет огибающей + val envelope = calculateEnvelope() + + // Обновление счетчиков в зависимости от состояния + when (envelopeState) { + EnvelopeState.ATTACK, EnvelopeState.DECAY, EnvelopeState.SUSTAIN -> { + samplesSinceNoteOn++ + } + EnvelopeState.RELEASE -> { + samplesSinceNoteOff++ + } + EnvelopeState.IDLE -> { + // Ничего не делаем + } + } + + // Проверяем переходы между состояниями + checkStateTransitions() + + // Если звук полностью затух, возвращаем 0 + if (envelopeState == EnvelopeState.IDLE) { + return 0.0 + } + + // Генерация waveform с сохранением фазы для избежания щелчков + val wave = calculateTubaWaveform(currentFrequency) + + return wave * envelope + } + } + + private fun calculateEnvelope(): Double { + return when (envelopeState) { + EnvelopeState.ATTACK -> { + if (samplesSinceNoteOn < attackTime) { + // Быстрая атака + (samplesSinceNoteOn / attackTime.toDouble()).pow(0.5) + } else { + // Переход к DECAY будет обработан в checkStateTransitions + (attackTime / attackTime.toDouble()).pow(0.5) + } + } + EnvelopeState.DECAY -> { + val decayProgress = (samplesSinceNoteOn - attackTime) / decayTime.toDouble() + if (decayProgress < 1.0) { + // Плавный спад до сустейна + 1.0 - (1.0 - sustainLevel) * decayProgress + } else { + // Переход к SUSTAIN будет обработан в checkStateTransitions + sustainLevel + } + } + EnvelopeState.SUSTAIN -> { + sustainLevel + } + EnvelopeState.RELEASE -> { + val releaseProgress = samplesSinceNoteOff / releaseTime.toDouble() + if (releaseProgress < 1.0) { + // Экспоненциальный релиз + sustainLevel * exp(-6.0 * releaseProgress) + } else { + // Переход к IDLE будет обработан в checkStateTransitions + 0.0 + } + } + EnvelopeState.IDLE -> { + 0.0 + } + }.also { currentAmplitude = it } + } + + private fun checkStateTransitions() { + when (envelopeState) { + EnvelopeState.ATTACK -> { + if (samplesSinceNoteOn >= attackTime) { + envelopeState = EnvelopeState.DECAY + } + } + EnvelopeState.DECAY -> { + if (samplesSinceNoteOn >= attackTime + decayTime) { + envelopeState = EnvelopeState.SUSTAIN + } + } + EnvelopeState.SUSTAIN -> { + // SUSTAIN продолжается пока isNoteOn = true + if (!isNoteOn) { + envelopeState = EnvelopeState.RELEASE + samplesSinceNoteOff = 0 + } + } + EnvelopeState.RELEASE -> { + if (samplesSinceNoteOff >= releaseTime) { + envelopeState = EnvelopeState.IDLE + } + } + EnvelopeState.IDLE -> { + // Остаемся в IDLE + } + } + } + + private fun calculateTubaWaveform(freq: Float): Double { + // Используем фазовый накопитель для избежания щелчков при смене нот + val phaseIncrement = 2.0 * PI * freq / SAMPLE_RATE + phase += phaseIncrement + + // Сбрасываем фазу при переполнении для избежания потери точности + if (phase > 2.0 * PI) { + phase -= 2.0 * PI + } + + val range = when { + freq < 50 -> "very_low" + freq < 100 -> "low" + freq < 200 -> "middle" + else -> "high" + } + + val fundamental = sin(phase) + + val overtones = when (range) { + "very_low" -> { + sin(2.0 * phase) * 0.6 + + sin(3.0 * phase) * 0.5 + + sin(4.0 * phase) * 0.4 + } + "low" -> { + sin(2.0 * phase) * 0.5 + + sin(3.0 * phase) * 0.45 + + sin(4.0 * phase) * 0.4 + + sin(5.0 * phase) * 0.3 + } + "middle" -> { + sin(2.0 * phase) * 0.4 + + sin(3.0 * phase) * 0.4 + + sin(4.0 * phase) * 0.35 + + sin(5.0 * phase) * 0.25 + + sin(6.0 * phase) * 0.2 + } + else -> { + sin(2.0 * phase) * 0.3 + + sin(3.0 * phase) * 0.35 + + sin(4.0 * phase) * 0.3 + + sin(5.0 * phase) * 0.25 + + sin(6.0 * phase) * 0.2 + } + } + + val mixed = fundamental * 0.6 + overtones * 0.8 + return tanh(mixed * 1.2) / 1.2 + } + + fun isActive(): Boolean { + synchronized(lock) { + return isNoteOn || envelopeState != EnvelopeState.IDLE + } + } + + fun getCurrentFrequency(): Float { + synchronized(lock) { + return currentFrequency + } + } + + fun release() { + shouldStop = true + isPlaying = false + + // Безопасная остановка потока + currentThread?.let { thread -> + try { + thread.join(100) // Ждем завершения потока до 100ms + } catch (e: InterruptedException) { + thread.interrupt() + } + } + currentThread = null + + // Безопасное освобождение AudioTrack + audioTrack?.let { track -> + try { + if (track.playState == AudioTrack.PLAYSTATE_PLAYING) { + track.stop() + } + track.release() + } catch (e: Exception) { + e.printStackTrace() + } + } + audioTrack = null + } +} \ No newline at end of file diff --git a/app/src/main/java/net/sergeych/karabass/MainActivity.kt b/app/src/main/java/net/sergeych/karabass/MainActivity.kt new file mode 100644 index 0000000..6fb2ded --- /dev/null +++ b/app/src/main/java/net/sergeych/karabass/MainActivity.kt @@ -0,0 +1,65 @@ +package net.sergeych.karabass + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBars +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import net.sergeych.karabass.ui.theme.KarabassTheme + +class MainActivity : ComponentActivity() { + + private lateinit var tubaSynth: AdvancedTubaSynth + + private val tubaNotes = AdvancedTubaSynth.TUBA_NOTES + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + tubaSynth = AdvancedTubaSynth(this) + + // Примеры нот для тубы (в Hz) + enableEdgeToEdge() + setContent { + KarabassTheme { + Scaffold(modifier = + Modifier.padding(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()) + ) { innerPadding -> + val lowNodeIndex = 1 + val keysCount = 22 + PianoKeyboard( + tubaSynth, + AdvancedTubaSynth.TUBA_NOTES.slice(lowNodeIndex .. lowNodeIndex + keysCount).map { it.first}, + Modifier.padding(innerPadding).fillMaxSize() + ) + + } + } + } + } +} + +@Composable +fun Greeting(name: String, modifier: Modifier = Modifier) { + Text( + text = "Hello $name!", + modifier = modifier + ) +} + +@Preview(showBackground = true) +@Composable +fun GreetingPreview() { + KarabassTheme { + Greeting("Android") + } +} \ No newline at end of file diff --git a/app/src/main/java/net/sergeych/karabass/PianoKeyboard.kt b/app/src/main/java/net/sergeych/karabass/PianoKeyboard.kt new file mode 100644 index 0000000..0bd8679 --- /dev/null +++ b/app/src/main/java/net/sergeych/karabass/PianoKeyboard.kt @@ -0,0 +1,337 @@ +package net.sergeych.karabass + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.input.pointer.PointerId +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.input.pointer.positionChanged +import androidx.compose.ui.platform.LocalConfiguration + +@Composable +fun PianoKeyboard( + synth: AdvancedTubaSynth, + noteRange: List, + modifier: Modifier = Modifier +) { + val configuration = LocalConfiguration.current + val screenWidth = configuration.screenWidthDp + val screenHeight = configuration.screenHeightDp + + val isHorizontal = screenWidth > screenHeight + + // Фильтруем и сортируем ноты по частоте + val availableNotes = AdvancedTubaSynth.TUBA_NOTES + .filter { it.first in noteRange } + .sortedBy { it.second } + + // Разделяем на белые и черные клавиши + val whiteKeys = availableNotes.filter { !it.first.contains("#") } + val blackKeys = availableNotes.filter { it.first.contains("#") } + + var activeKeys by remember { mutableStateOf(emptySet()) } + + Box(modifier = modifier.fillMaxSize()) { + Canvas( + modifier = Modifier + .fillMaxSize() + .pointerInput(synth, whiteKeys, blackKeys, isHorizontal, availableNotes) { + awaitPointerEventScope { + // Карта для отслеживания активных указателей и их нот + val activePointers = mutableMapOf() + + while (true) { + val event = awaitPointerEvent() + + // Обрабатываем все изменения в событии + for (change in event.changes) { + val pointerId = change.id + val position = change.position + val canvasSize = Size(size.width.toFloat(), size.height.toFloat()) + + // Находим клавишу под указателем + val key = findKeyAtOffset( + offset = position, + size = canvasSize, + whiteKeys = whiteKeys, + blackKeys = blackKeys, + isHorizontal = isHorizontal + ) + + when { + // НАЖАТИЕ - когда указатель только что нажат + change.pressed -> { + key?.let { (noteName, frequency) -> + // Запоминаем связь указатель-нота + activePointers[pointerId] = noteName + activeKeys = activeKeys + noteName + + println("startNote $noteName $frequency") + if (synth.isActive()) { + // Легато - плавный переход на новую ноту + synth.changeNote(frequency) + } else { + // Новая нота + synth.startNote(frequency) + } + } + } + + // ОТПУСКАНИЕ - когда указатель отпущен + !change.pressed -> { + val releasedNoteName = activePointers.remove(pointerId) + releasedNoteName?.let { noteName -> + activeKeys = activeKeys - noteName + + if (activePointers.isEmpty()) { + // Все клавиши отпущены - останавливаем ноту + synth.stopNote() + } else { + // Есть другие активные клавиши - переключаемся на одну из них + val remainingNoteName = activePointers.values.firstOrNull() + remainingNoteName?.let { name -> + val remainingNote = availableNotes.find { it.first == name } + remainingNote?.let { + synth.changeNote(it.second) + } + } + } + } + } + + // ПЕРЕМЕЩЕНИЕ - когда указатель переместился + change.positionChanged() -> { + key?.let { (newNoteName, newFrequency) -> + val currentNoteName = activePointers[pointerId] + + // Если указатель переместился на другую клавишу + if (currentNoteName != newNoteName) { + // Обновляем активные клавиши + currentNoteName?.let { + activeKeys = activeKeys - it + } + activeKeys = activeKeys + newNoteName + activePointers[pointerId] = newNoteName + + // Плавно переключаем ноту + if (synth.isActive()) { + synth.changeNote(newFrequency) + } + } + } + } + } + + // Всегда сообщаем, что обработали событие + change.consume() + } + } + } + } + ) { + if (isHorizontal) { + drawHorizontalKeyboard( + whiteKeys = whiteKeys, + blackKeys = blackKeys, + activeKeys = activeKeys, + size = size + ) + } else { + drawVerticalKeyboard( + whiteKeys = whiteKeys, + blackKeys = blackKeys, + activeKeys = activeKeys, + size = size + ) + } + } + } +} + +// Вспомогательные функции остаются без изменений: + +private fun findKeyAtOffset( + offset: Offset, + size: Size, + whiteKeys: List>, + blackKeys: List>, + isHorizontal: Boolean +): Pair? { + return if (isHorizontal) { + findKeyAtOffsetHorizontal(offset, size, whiteKeys, blackKeys) + } else { + findKeyAtOffsetVertical(offset, size, whiteKeys, blackKeys) + } +} + +private fun findKeyAtOffsetHorizontal( + offset: Offset, + size: Size, + whiteKeys: List>, + blackKeys: List> +): Pair? { + val whiteKeyWidth = size.width / whiteKeys.size + + // Сначала проверяем черные клавиши (они сверху и могут перекрывать белые) + blackKeys.forEachIndexed { index, blackKey -> + val blackKeyX = (getWhiteKeyIndexForBlackKey(blackKey.first, whiteKeys) * whiteKeyWidth) - + (whiteKeyWidth * 0.25f) + val blackKeyRect = Rect( + left = blackKeyX, + top = 0f, + right = blackKeyX + whiteKeyWidth * 0.5f, + bottom = size.height * 0.6f + ) + if (blackKeyRect.contains(offset)) { + return blackKey + } + } + + // Затем проверяем белые клавиши + whiteKeys.forEachIndexed { index, whiteKey -> + val whiteKeyRect = Rect( + left = index * whiteKeyWidth, + top = 0f, + right = (index + 1) * whiteKeyWidth, + bottom = size.height + ) + if (whiteKeyRect.contains(offset)) { + return whiteKey + } + } + + return null +} + +private fun findKeyAtOffsetVertical( + offset: Offset, + size: Size, + whiteKeys: List>, + blackKeys: List> +): Pair? { + val whiteKeyHeight = size.height / whiteKeys.size + + // Сначала проверяем черные клавиши (они справа и могут перекрывать белые) + blackKeys.forEachIndexed { index, blackKey -> + val blackKeyY = (getWhiteKeyIndexForBlackKey(blackKey.first, whiteKeys) * whiteKeyHeight) - + (whiteKeyHeight * 0.25f) + val blackKeyRect = Rect( + left = size.width * 0.4f, + top = blackKeyY, + right = size.width, + bottom = blackKeyY + whiteKeyHeight * 0.5f + ) + if (blackKeyRect.contains(offset)) { + return blackKey + } + } + + // Затем проверяем белые клавиши + whiteKeys.forEachIndexed { index, whiteKey -> + val whiteKeyRect = Rect( + left = 0f, + top = index * whiteKeyHeight, + right = size.width, + bottom = (index + 1) * whiteKeyHeight + ) + if (whiteKeyRect.contains(offset)) { + return whiteKey + } + } + + return null +} + +private fun getWhiteKeyIndexForBlackKey(blackKeyName: String, whiteKeys: List>): Int { + // Для черной клавиши находим соответствующую белую клавишу справа + val baseNote = blackKeyName.replace("#", "") + val whiteKeyIndex = whiteKeys.indexOfFirst { it.first == baseNote } + return if (whiteKeyIndex >= 0) whiteKeyIndex + 1 else 0 +} + +private fun DrawScope.drawHorizontalKeyboard( + whiteKeys: List>, + blackKeys: List>, + activeKeys: Set, + size: Size +) { + val whiteKeyWidth = size.width / whiteKeys.size + + // Рисуем белые клавиши + whiteKeys.forEachIndexed { index, (noteName, _) -> + val isActive = noteName in activeKeys + drawRect( + color = if (isActive) Color(0xFF64B5F6) else Color.White, + topLeft = Offset(x = index * whiteKeyWidth, y = 0f), + size = Size(width = whiteKeyWidth, height = size.height) + ) + + // Контур белой клавиши + drawRect( + color = Color.Black, + topLeft = Offset(x = index * whiteKeyWidth, y = 0f), + size = Size(width = 1f, height = size.height), + alpha = 0.3f + ) + } + + // Рисуем черные клавиши + blackKeys.forEachIndexed { index, (noteName, _) -> + val isActive = noteName in activeKeys + val whiteKeyIndex = getWhiteKeyIndexForBlackKey(noteName, whiteKeys) + val blackKeyX = (whiteKeyIndex * whiteKeyWidth) - (whiteKeyWidth * 0.25f) + + drawRect( + color = if (isActive) Color(0xFF1976D2) else Color.Black, + topLeft = Offset(x = blackKeyX, y = 0f), + size = Size(width = whiteKeyWidth * 0.5f, height = size.height * 0.6f) + ) + } +} + +private fun DrawScope.drawVerticalKeyboard( + whiteKeys: List>, + blackKeys: List>, + activeKeys: Set, + size: Size +) { + val whiteKeyHeight = size.height / whiteKeys.size + + // Рисуем белые клавиши + whiteKeys.forEachIndexed { index, (noteName, _) -> + val isActive = noteName in activeKeys + drawRect( + color = if (isActive) Color(0xFF64B5F6) else Color.White, + topLeft = Offset(x = 0f, y = index * whiteKeyHeight), + size = Size(width = size.width, height = whiteKeyHeight) + ) + + // Контур белой клавиши + drawRect( + color = Color.Black, + topLeft = Offset(x = 0f, y = index * whiteKeyHeight), + size = Size(width = size.width, height = 1f), + alpha = 0.3f + ) + } + + // Рисуем черные клавиши + blackKeys.forEachIndexed { index, (noteName, _) -> + val isActive = noteName in activeKeys + val whiteKeyIndex = getWhiteKeyIndexForBlackKey(noteName, whiteKeys) + val blackKeyY = (whiteKeyIndex * whiteKeyHeight) - (whiteKeyHeight * 0.25f) + + drawRect( + color = if (isActive) Color(0xFF1976D2) else Color.Black, + topLeft = Offset(x = size.width * 0.4f, y = blackKeyY), + size = Size(width = size.width * 0.6f, height = whiteKeyHeight * 0.5f) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/net/sergeych/karabass/RealisticTubaSynth.kt b/app/src/main/java/net/sergeych/karabass/RealisticTubaSynth.kt new file mode 100644 index 0000000..0ce706f --- /dev/null +++ b/app/src/main/java/net/sergeych/karabass/RealisticTubaSynth.kt @@ -0,0 +1,260 @@ +import android.content.Context +import android.media.AudioAttributes +import android.media.AudioFormat +import android.media.AudioManager +import android.media.AudioTrack +import android.os.Build +import kotlin.math.* +import kotlin.random.Random + +class RealisticTubaSynth(private val context: Context) { + + companion object { + const val SAMPLE_RATE = 44100 + const val CUTOFF_FREQ = 800.0 + } + + private var audioTrack: AudioTrack? = null + private var isPlaying = false + private var currentThread: Thread? = null + + data class TubaNote( + val frequency: Float, + val amplitude: Float = 0.8f, + val durationMs: Int + ) + + fun playTubaNote(frequency: Float, durationMs: Int = 1500) { + // Останавливаем предыдущее воспроизведение + stop() + + val note = TubaNote(frequency, 0.8f, durationMs) + generateTubaSound(note) + } + + private fun generateTubaSound(note: TubaNote) { + currentThread = Thread { + isPlaying = true + val numSamples = note.durationMs * SAMPLE_RATE / 1000 + val buffer = ShortArray(numSamples) + + // Фильтр для шума дыхания + val alpha = 1 - exp(-2.0 * PI * CUTOFF_FREQ / SAMPLE_RATE) + var filteredNoise = 0.0 + + val random = Random(System.currentTimeMillis()) + + for (i in 0 until numSamples) { + if (!isPlaying) break + + val time = i.toDouble() / SAMPLE_RATE + + // Основной waveform тубы с более богатым спектром + val tubaWave = calculateTubaWaveform(note.frequency, time) + + // Шум дыхания с фильтром низких частот + val whiteNoise = random.nextDouble() * 2 - 1 + filteredNoise += alpha * (whiteNoise - filteredNoise) + + // Огибающая с резкой атакой и быстрым спадом + val envelope = getRealisticTubaEnvelope(i, numSamples, note.frequency) + + // Усиление басов + val bassBoost = 1.0 + 0.5 * sin(2 * PI * 60.0 * time) + + // Смешиваем всё вместе + var sample = tubaWave + filteredNoise * 0.03 * envelope + sample *= envelope * bassBoost + + // Ограничиваем амплитуду + sample = sample.coerceIn(-1.0, 1.0) + + buffer[i] = (sample * Short.MAX_VALUE).toInt().toShort() + } + + if (isPlaying) { + playAudioBuffer(buffer) + } + }.apply { + start() + } + } + + private fun calculateTubaWaveform(freq: Float, time: Double): Double { + // Классификация нот по диапазонам + val range = when { + freq < 40 -> "pedal" // Педальные ноты (очень низкие) + freq < 80 -> "low" // Низкий диапазон + freq < 160 -> "middle" // Средний диапазон + freq < 240 -> "high" // Высокий диапазон + else -> "very_high" // Очень высокий диапазон + } + + val fundamental = sin(2 * PI * freq * time) + + // Разные наборы обертонов для разных диапазонов + val overtones = when (range) { + "pedal" -> { + // Педальные ноты: сильные низкие обертоны, меньше высоких + sin(2 * PI * freq * 2 * time) * 0.6 + + sin(2 * PI * freq * 3 * time) * 0.5 + + sin(2 * PI * freq * 4 * time) * 0.4 + + sin(2 * PI * freq * 5 * time) * 0.3 + } + "low" -> { + // Низкий диапазон: богатый спектр + sin(2 * PI * freq * 2 * time) * 0.5 + + sin(2 * PI * freq * 3 * time) * 0.45 + + sin(2 * PI * freq * 4 * time) * 0.4 + + sin(2 * PI * freq * 5 * time) * 0.35 + + sin(2 * PI * freq * 6 * time) * 0.25 + } + "middle" -> { + // Средний диапазон: сбалансированный спектр + sin(2 * PI * freq * 2 * time) * 0.4 + + sin(2 * PI * freq * 3 * time) * 0.4 + + sin(2 * PI * freq * 4 * time) * 0.35 + + sin(2 * PI * freq * 5 * time) * 0.3 + + sin(2 * PI * freq * 6 * time) * 0.25 + + sin(2 * PI * freq * 7 * time) * 0.2 + } + "high" -> { + // Высокий диапазон: больше высоких обертонов + sin(2 * PI * freq * 2 * time) * 0.3 + + sin(2 * PI * freq * 3 * time) * 0.35 + + sin(2 * PI * freq * 4 * time) * 0.3 + + sin(2 * PI * freq * 5 * time) * 0.25 + + sin(2 * PI * freq * 6 * time) * 0.2 + + sin(2 * PI * freq * 7 * time) * 0.15 + + sin(2 * PI * freq * 8 * time) * 0.1 + } + else -> { + // Очень высокий диапазон: яркий, с преобладанием высоких обертонов + sin(2 * PI * freq * 2 * time) * 0.25 + + sin(2 * PI * freq * 3 * time) * 0.3 + + sin(2 * PI * freq * 4 * time) * 0.25 + + sin(2 * PI * freq * 5 * time) * 0.2 + + sin(2 * PI * freq * 6 * time) * 0.15 + + sin(2 * PI * freq * 7 * time) * 0.1 + + sin(2 * PI * freq * 8 * time) * 0.08 + + sin(2 * PI * freq * 9 * time) * 0.05 + } + } + + val mixed = fundamental * 0.6 + overtones * 0.8 + return tanh(mixed * 1.2) / 1.2 + } + + private fun getRealisticTubaEnvelope(sampleIndex: Int, totalSamples: Int, frequency: Float): Double { + val position = sampleIndex.toDouble() / totalSamples + + // Разная огибающая для разных диапазонов + val (attack, decay, sustain, release) = when { + frequency < 40 -> arrayOf(0.04, 0.2, 0.6, 0.15) // Педальные ноты: медленнее + frequency < 100 -> arrayOf(0.025, 0.15, 0.65, 0.1) // Низкие: умеренные + frequency < 200 -> arrayOf(0.015, 0.1, 0.7, 0.08) // Средние: быстрее + else -> arrayOf(0.01, 0.08, 0.75, 0.06) // Высокие: очень быстрые + } + + return when { + position < attack -> (position / attack).pow(0.3) + position < attack + decay -> { + val decayPos = (position - attack) / decay + 1.0 - (1.0 - sustain) * decayPos + } + position > 1 - release -> { + val releasePos = (position - (1 - release)) / release + sustain * (1 - releasePos) + } + else -> sustain + } + } + private fun playAudioBuffer(buffer: ShortArray) { + try { + // Создаем новый AudioTrack каждый раз + val bufferSize = AudioTrack.getMinBufferSize( + SAMPLE_RATE, + AudioFormat.CHANNEL_OUT_MONO, + AudioFormat.ENCODING_PCM_16BIT + ) + + val newAudioTrack = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val attributes = AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_MEDIA) + .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) + .build() + + val format = AudioFormat.Builder() + .setSampleRate(SAMPLE_RATE) + .setEncoding(AudioFormat.ENCODING_PCM_16BIT) + .setChannelMask(AudioFormat.CHANNEL_OUT_MONO) + .build() + + AudioTrack.Builder() + .setAudioAttributes(attributes) + .setAudioFormat(format) + .setBufferSizeInBytes(bufferSize) + .build() + } else { + @Suppress("DEPRECATION") + AudioTrack( + AudioManager.STREAM_MUSIC, + SAMPLE_RATE, + AudioFormat.CHANNEL_OUT_MONO, + AudioFormat.ENCODING_PCM_16BIT, + bufferSize, + AudioTrack.MODE_STATIC + ) + } + + newAudioTrack.apply { + setVolume(0.8f) + + // Используем MODE_STATIC для однократного воспроизведения + write(buffer, 0, buffer.size) + + setPlaybackPositionUpdateListener(object : AudioTrack.OnPlaybackPositionUpdateListener { + override fun onMarkerReached(track: AudioTrack) { + // Автоматически освобождаем после воспроизведения + track.stop() + track.release() + } + + override fun onPeriodicNotification(track: AudioTrack) {} + }) + + setNotificationMarkerPosition(buffer.size) + play() + } + + audioTrack = newAudioTrack + + } catch (e: Exception) { + e.printStackTrace() + isPlaying = false + } + } + + fun stop() { + isPlaying = false + currentThread?.interrupt() + currentThread = null + + audioTrack?.let { track -> + try { + if (track.playState == AudioTrack.PLAYSTATE_PLAYING) { + track.stop() + } + track.release() + } catch (e: IllegalStateException) { + // Игнорируем ошибки при остановке уже остановленного трека + e.printStackTrace() + } + } + audioTrack = null + } + + fun release() { + stop() + } +} \ No newline at end of file diff --git a/app/src/main/java/net/sergeych/karabass/TubaSynth.kt b/app/src/main/java/net/sergeych/karabass/TubaSynth.kt new file mode 100644 index 0000000..6247366 --- /dev/null +++ b/app/src/main/java/net/sergeych/karabass/TubaSynth.kt @@ -0,0 +1,199 @@ +package net.sergeych.karabass + +import android.content.Context +import android.media.AudioAttributes +import android.media.AudioFormat +import android.media.AudioManager +import android.media.AudioTrack +import android.os.Build +import kotlin.math.* +import kotlin.random.Random + +class TubaSynth(private val context: Context) { + + companion object { + const val SAMPLE_RATE = 44100 + const val CUTOFF_FREQ = 500.0 + } + + private var audioTrack: AudioTrack? = null + private var isPlaying = false + + data class TubaNote( + val frequency: Float, + val amplitude: Float = 0.8f, + val durationMs: Int + ) + + fun playTubaNote(frequency: Float, durationMs: Int = 2000) { + if (isPlaying) { + stop() + } + + val note = TubaNote(frequency, 0.8f, durationMs) + generateTubaSound(note) + } + + private fun generateTubaSound(note: TubaNote) { + Thread { + isPlaying = true + val numSamples = note.durationMs * SAMPLE_RATE / 1000 + val buffer = ShortArray(numSamples) + + // Фильтр для шума дыхания + val alpha = 1 - exp(-2.0 * PI * CUTOFF_FREQ / SAMPLE_RATE) + var filteredNoise = 0.0 + + val random = Random(System.currentTimeMillis()) + + for (i in 0 until numSamples) { + if (!isPlaying) break + + val time = i.toDouble() / SAMPLE_RATE + + // Основной waveform тубы + val tubaWave = calculateTubaWaveform(note.frequency, time) + + // Шум дыхания с фильтром низких частот + val whiteNoise = random.nextDouble() * 2 - 1 + filteredNoise += alpha * (whiteNoise - filteredNoise) + + // Огибающая + val envelope = getTubaEnvelope(i, numSamples) + + // Усиление басов + val bassBoost = 1.0 + 0.4 * sin(2 * PI * 80.0 * time) + + // Смешиваем всё вместе + var sample = tubaWave + filteredNoise * 0.05 * envelope + sample *= envelope * bassBoost + + // Ограничиваем амплитуду + sample = sample.coerceIn(-1.0, 1.0) + + buffer[i] = (sample * Short.MAX_VALUE).toInt().toShort() + } + + if (isPlaying) { + playAudioBuffer(buffer) + } + }.start() + } + + private fun calculateTubaWaveform(freq: Float, time: Double): Double { + return ( + sin(2 * PI * freq * time) * 0.6 + + sin(2 * PI * freq * 2 * time) * 0.4 + + sin(2 * PI * freq * 3 * time) * 0.3 + + sin(2 * PI * freq * 4 * time) * 0.2 + + sin(2 * PI * freq * 5 * time) * 0.15 + + sin(2 * PI * freq * 6 * time) * 0.1 + + sin(2 * PI * freq * 7 * time) * 0.05 + ) * 0.6 + } + + private fun getTubaEnvelope(sampleIndex: Int, totalSamples: Int): Double { + val position = sampleIndex.toDouble() / totalSamples + + val attack = 0.15 + val decay = 0.1 + val release = 0.3 + + return when { + position < attack -> { + val x = position / attack + x * x * (3 - 2 * x) + } + position < attack + decay -> { + val decayPos = (position - attack) / decay + 0.9 + 0.1 * (1 - decayPos) + } + position > 1 - release -> { + val releasePos = (position - (1 - release)) / release + (1 - releasePos) * 0.9 + } + else -> 0.9 + } + } + + private fun playAudioBuffer(buffer: ShortArray) { + try { + // Останавливаем предыдущее воспроизведение + audioTrack?.stop() + audioTrack?.release() + + val bufferSize = AudioTrack.getMinBufferSize( + SAMPLE_RATE, + AudioFormat.CHANNEL_OUT_MONO, + AudioFormat.ENCODING_PCM_16BIT + ) + + // Создаем AudioTrack с правильными параметрами + audioTrack = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val attributes = AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_MEDIA) + .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) + .build() + + val format = AudioFormat.Builder() + .setSampleRate(SAMPLE_RATE) + .setEncoding(AudioFormat.ENCODING_PCM_16BIT) + .setChannelMask(AudioFormat.CHANNEL_OUT_MONO) + .build() + + AudioTrack.Builder() + .setAudioAttributes(attributes) + .setAudioFormat(format) + .setBufferSizeInBytes(bufferSize) + .build() + } else { + @Suppress("DEPRECATION") + AudioTrack( + AudioManager.STREAM_MUSIC, + SAMPLE_RATE, + AudioFormat.CHANNEL_OUT_MONO, + AudioFormat.ENCODING_PCM_16BIT, + bufferSize, + AudioTrack.MODE_STREAM + ) + } + + audioTrack?.apply { + // Устанавливаем громкость + setVolume(0.8f) + + // Воспроизводим + play() + + // Пишем данные + write(buffer, 0, buffer.size) + + // Ждем окончания воспроизведения + setNotificationMarkerPosition(buffer.size) + setPlaybackPositionUpdateListener(object : AudioTrack.OnPlaybackPositionUpdateListener { + override fun onMarkerReached(track: AudioTrack) { + stop() + release() + isPlaying = false + } + + override fun onPeriodicNotification(track: AudioTrack) {} + }) + } + } catch (e: Exception) { + e.printStackTrace() + isPlaying = false + } + } + + fun stop() { + isPlaying = false + audioTrack?.stop() + } + + fun release() { + stop() + audioTrack?.release() + audioTrack = null + } +} \ No newline at end of file diff --git a/app/src/main/java/net/sergeych/karabass/ui/theme/Color.kt b/app/src/main/java/net/sergeych/karabass/ui/theme/Color.kt new file mode 100644 index 0000000..1996c26 --- /dev/null +++ b/app/src/main/java/net/sergeych/karabass/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package net.sergeych.karabass.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) \ No newline at end of file diff --git a/app/src/main/java/net/sergeych/karabass/ui/theme/Theme.kt b/app/src/main/java/net/sergeych/karabass/ui/theme/Theme.kt new file mode 100644 index 0000000..e3770ed --- /dev/null +++ b/app/src/main/java/net/sergeych/karabass/ui/theme/Theme.kt @@ -0,0 +1,58 @@ +package net.sergeych.karabass.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 KarabassTheme( + 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 + ) +} \ No newline at end of file diff --git a/app/src/main/java/net/sergeych/karabass/ui/theme/Type.kt b/app/src/main/java/net/sergeych/karabass/ui/theme/Type.kt new file mode 100644 index 0000000..4456ea2 --- /dev/null +++ b/app/src/main/java/net/sergeych/karabass/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package net.sergeych.karabass.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 + ) + */ +) \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..c8aab94 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + karabass + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..accc715 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +