Compare commits

...

3 Commits

Author SHA1 Message Date
a78093c6b8 fixed segfault bug 2025-10-13 15:15:57 +04:00
37ec0f7a6d launcher icon 2025-10-13 14:59:49 +04:00
c4cf8749fb fixed bug with keyboard 2025-10-13 14:48:21 +04:00
24 changed files with 191 additions and 88 deletions

1
.gitignore vendored
View File

@ -9,3 +9,4 @@
.externalNativeBuild .externalNativeBuild
.cxx .cxx
local.properties local.properties
/app/release/

View File

@ -6,9 +6,9 @@
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher2"
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher2_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.Karabass"> android:theme="@style/Theme.Karabass">
<activity <activity

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 KiB

View File

@ -1,5 +1,4 @@
package net.sergeych.karabass package net.sergeych.karabass
import android.content.Context import android.content.Context
import android.media.AudioAttributes import android.media.AudioAttributes
import android.media.AudioFormat import android.media.AudioFormat
@ -40,6 +39,7 @@ class AdvancedTubaSynth(private val context: Context) {
@Volatile private var audioTrack: AudioTrack? = null @Volatile private var audioTrack: AudioTrack? = null
@Volatile private var isPlaying = false @Volatile private var isPlaying = false
@Volatile private var shouldStop = false @Volatile private var shouldStop = false
@Volatile private var isReleased = false
private var currentThread: Thread? = null private var currentThread: Thread? = null
// Синхронизированные переменные для потокобезопасности // Синхронизированные переменные для потокобезопасности
@ -55,23 +55,24 @@ class AdvancedTubaSynth(private val context: Context) {
private var envelopeState = 0 // 0: idle, 1: attack, 2: release private var envelopeState = 0 // 0: idle, 1: attack, 2: release
// Параметры огибающей // Параметры огибающей
private val attackSamples = (0.02 * SAMPLE_RATE).toInt() // Увеличил до 20ms для более плавного старта private val attackSamples = (0.02 * SAMPLE_RATE).toInt()
private val releaseSamples = (0.2 * SAMPLE_RATE).toInt() // 200ms релиз private val releaseSamples = (0.2 * SAMPLE_RATE).toInt()
// Минимальный буфер для low-latency // Минимальный буфер для low-latency
private val bufferSize = 1024 // Увеличил буфер для стабильности private val bufferSize = 1024
// Для управления выводом // Для управления выводом
private var lastBufferWritten = false
private var silenceBuffersWritten = 0 private var silenceBuffersWritten = 0
private val requiredSilenceBuffers = 3 // Дополнительные буферы тишины после затухания private val requiredSilenceBuffers = 3
init { init {
initializeAudioTrack() initializeAudioTrack()
} }
private fun initializeAudioTrack() { private fun initializeAudioTrack(): Boolean {
synchronized(lock) { synchronized(lock) {
if (isReleased) return false
try { try {
// Освобождаем старый AudioTrack если есть // Освобождаем старый AudioTrack если есть
audioTrack?.let { track -> audioTrack?.let { track ->
@ -81,11 +82,11 @@ class AdvancedTubaSynth(private val context: Context) {
} }
track.release() track.release()
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() // Игнорируем ошибки при освобождении
} }
} }
audioTrack = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { val newTrack = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val attributes = AudioAttributes.Builder() val attributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA) .setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
@ -100,7 +101,7 @@ class AdvancedTubaSynth(private val context: Context) {
AudioTrack.Builder() AudioTrack.Builder()
.setAudioAttributes(attributes) .setAudioAttributes(attributes)
.setAudioFormat(format) .setAudioFormat(format)
.setBufferSizeInBytes(bufferSize * 4) // Увеличил размер буфера .setBufferSizeInBytes(bufferSize * 4)
.build() .build()
} else { } else {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
@ -109,22 +110,29 @@ class AdvancedTubaSynth(private val context: Context) {
SAMPLE_RATE, SAMPLE_RATE,
AudioFormat.CHANNEL_OUT_MONO, AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT, AudioFormat.ENCODING_PCM_16BIT,
bufferSize * 4, // Увеличил размер буфера bufferSize * 4,
AudioTrack.MODE_STREAM AudioTrack.MODE_STREAM
) )
} }
audioTrack?.play() if (newTrack.state == AudioTrack.STATE_INITIALIZED) {
audioTrack = newTrack
newTrack.play()
return true
} else {
newTrack.release()
return false
}
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({ return false
initializeAudioTrack()
}, 100)
} }
} }
} }
fun startNote(frequency: Float) { fun startNote(frequency: Float) {
if (isReleased) return
synchronized(lock) { synchronized(lock) {
targetFrequency = frequency targetFrequency = frequency
@ -135,7 +143,6 @@ class AdvancedTubaSynth(private val context: Context) {
envelopeSamples = 0 envelopeSamples = 0
envelopeValue = 0.0 envelopeValue = 0.0
currentFrequency = frequency currentFrequency = frequency
lastBufferWritten = false
silenceBuffersWritten = 0 silenceBuffersWritten = 0
// Сбрасываем фазу при начале новой ноты для предотвращения щелчков // Сбрасываем фазу при начале новой ноты для предотвращения щелчков
@ -152,6 +159,8 @@ class AdvancedTubaSynth(private val context: Context) {
} }
fun stopNote() { fun stopNote() {
if (isReleased) return
synchronized(lock) { synchronized(lock) {
if (isNoteOn) { if (isNoteOn) {
isNoteOn = false isNoteOn = false
@ -162,29 +171,39 @@ class AdvancedTubaSynth(private val context: Context) {
} }
fun changeNote(frequency: Float) { fun changeNote(frequency: Float) {
if (isReleased) return
synchronized(lock) { synchronized(lock) {
targetFrequency = frequency targetFrequency = frequency
} }
} }
private fun startAudioGeneration() { private fun startAudioGeneration() {
if (isPlaying) return if (isPlaying || isReleased) return
shouldStop = false shouldStop = false
isPlaying = true isPlaying = true
lastBufferWritten = false
silenceBuffersWritten = 0
// Проверяем и при необходимости пересоздаем AudioTrack
val track = audioTrack val track = audioTrack
if (track == null || track.state != AudioTrack.STATE_INITIALIZED) { if (track == null || track.state != AudioTrack.STATE_INITIALIZED) {
initializeAudioTrack() if (!initializeAudioTrack()) {
isPlaying = false
return
}
} }
currentThread = Thread { currentThread = Thread {
val buffer = ShortArray(bufferSize) val buffer = ShortArray(bufferSize)
var currentAudioTrack: AudioTrack? = null
try { try {
while (!shouldStop && isPlaying) { // Получаем ссылку на AudioTrack один раз в начале
synchronized(lock) {
currentAudioTrack = audioTrack
}
while (!shouldStop && isPlaying && !isReleased) {
// Заполняем буфер // Заполняем буфер
var allSamplesZero = true var allSamplesZero = true
for (i in buffer.indices) { for (i in buffer.indices) {
@ -196,7 +215,7 @@ class AdvancedTubaSynth(private val context: Context) {
} }
// Безопасная запись в AudioTrack // Безопасная запись в AudioTrack
val track = audioTrack val track = currentAudioTrack
if (track?.state == AudioTrack.STATE_INITIALIZED && track.playState == AudioTrack.PLAYSTATE_PLAYING) { if (track?.state == AudioTrack.STATE_INITIALIZED && track.playState == AudioTrack.PLAYSTATE_PLAYING) {
try { try {
track.write(buffer, 0, buffer.size) track.write(buffer, 0, buffer.size)
@ -204,18 +223,29 @@ class AdvancedTubaSynth(private val context: Context) {
// Если это последний буфер с данными, запоминаем это // Если это последний буфер с данными, запоминаем это
if (allSamplesZero && envelopeState == 0) { if (allSamplesZero && envelopeState == 0) {
silenceBuffersWritten++ silenceBuffersWritten++
if (silenceBuffersWritten >= requiredSilenceBuffers) {
lastBufferWritten = true
}
} else { } else {
silenceBuffersWritten = 0 silenceBuffersWritten = 0
lastBufferWritten = false
} }
} catch (e: Exception) { } catch (e: Exception) {
if (!shouldStop) { if (!shouldStop && !isReleased) {
e.printStackTrace() // Пытаемся восстановить AudioTrack
android.os.Handler(android.os.Looper.getMainLooper()).post { synchronized(lock) {
initializeAudioTrack() if (initializeAudioTrack()) {
currentAudioTrack = audioTrack
} else {
break
}
}
}
}
} else {
// AudioTrack не готов
if (!shouldStop && !isReleased) {
synchronized(lock) {
if (initializeAudioTrack()) {
currentAudioTrack = audioTrack
} else {
break
} }
} }
} }
@ -225,12 +255,11 @@ class AdvancedTubaSynth(private val context: Context) {
// 1. Огибающая завершила релиз // 1. Огибающая завершила релиз
// 2. Мы записали несколько буферов тишины для гарантии // 2. Мы записали несколько буферов тишины для гарантии
// 3. Нет активной ноты // 3. Нет активной ноты
if (lastBufferWritten) { if (silenceBuffersWritten >= requiredSilenceBuffers) {
synchronized(lock) { synchronized(lock) {
if (!isNoteOn && envelopeState == 0) { if (!isNoteOn && envelopeState == 0) {
// Даем дополнительное время на вывод последних буферов
Thread.sleep(50)
isPlaying = false isPlaying = false
break
} }
} }
} }
@ -247,6 +276,8 @@ class AdvancedTubaSynth(private val context: Context) {
private fun generateSample(): Double { private fun generateSample(): Double {
synchronized(lock) { synchronized(lock) {
if (isReleased) return 0.0
// Плавное изменение частоты для легато // Плавное изменение частоты для легато
if (abs(currentFrequency - targetFrequency) > 0.1) { if (abs(currentFrequency - targetFrequency) > 0.1) {
currentFrequency += (targetFrequency - currentFrequency) * 0.2f currentFrequency += (targetFrequency - currentFrequency) * 0.2f
@ -318,21 +349,13 @@ class AdvancedTubaSynth(private val context: Context) {
// Смешиваем с весами, характерными для тубы // Смешиваем с весами, характерными для тубы
val mixed = fundamental * 0.7 + overtone2 * 0.5 + overtone3 * 0.3 + overtone4 * 0.2 + overtone5 * 0.15 val mixed = fundamental * 0.7 + overtone2 * 0.5 + overtone3 * 0.3 + overtone4 * 0.2 + overtone5 * 0.15
val result = tanh(mixed)
// Очень мягкое ограничение на резкие переходы
return if (abs(result) > 0.8) {
result * 0.99 // Слегка уменьшаем пики
} else {
result
}
// Очень мягкое ограничение для теплого звука // Очень мягкое ограничение для теплого звука
// return tanh(mixed) return tanh(mixed)
} }
fun isActive(): Boolean { fun isActive(): Boolean {
synchronized(lock) { synchronized(lock) {
return isNoteOn || envelopeState != 0 return !isReleased && (isNoteOn || envelopeState != 0)
} }
} }
@ -343,20 +366,14 @@ class AdvancedTubaSynth(private val context: Context) {
} }
fun release() { fun release() {
isReleased = true
shouldStop = true shouldStop = true
isPlaying = false isPlaying = false
// Даем время на завершение релиза и вывод всех буферов // Останавливаем поток генерации
try {
Thread.sleep(300)
} catch (e: InterruptedException) {
// Игнорируем
}
// Безопасная остановка потока
currentThread?.let { thread -> currentThread?.let { thread ->
try { try {
thread.join(200) thread.join(500) // Увеличил время ожидания
} catch (e: InterruptedException) { } catch (e: InterruptedException) {
thread.interrupt() thread.interrupt()
} }
@ -375,7 +392,7 @@ class AdvancedTubaSynth(private val context: Context) {
} }
track.release() track.release()
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() // Игнорируем ошибки при освобождении
} }
} }
audioTrack = null audioTrack = null

View File

@ -1,12 +1,10 @@
package net.sergeych.karabass package net.sergeych.karabass
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.DrawScope
@ -44,14 +42,16 @@ fun PianoKeyboard(
.fillMaxSize() .fillMaxSize()
.pointerInput(synth, whiteKeys, blackKeys, isHorizontal, availableNotes) { .pointerInput(synth, whiteKeys, blackKeys, isHorizontal, availableNotes) {
awaitPointerEventScope { awaitPointerEventScope {
// Карта для отслеживания активных указателей и их нот // Карта для отслеживания активных указателей и их текущих нот
val activePointers = mutableMapOf<PointerId, String>() val activePointers = mutableMapOf<PointerId, Pair<String, Float>>()
while (true) { while (true) {
val event = awaitPointerEvent() val event = awaitPointerEvent()
// Обрабатываем все изменения в событии // Временный набор для обновления активных клавиш
for (change in event.changes) { val newActiveKeys = mutableSetOf<String>()
event.changes.forEach { change ->
val pointerId = change.id val pointerId = change.id
val position = change.position val position = change.position
val canvasSize = Size(size.width.toFloat(), size.height.toFloat()) val canvasSize = Size(size.width.toFloat(), size.height.toFloat())
@ -70,10 +70,9 @@ fun PianoKeyboard(
change.pressed -> { change.pressed -> {
key?.let { (noteName, frequency) -> key?.let { (noteName, frequency) ->
// Запоминаем связь указатель-нота // Запоминаем связь указатель-нота
activePointers[pointerId] = noteName activePointers[pointerId] = noteName to frequency
activeKeys = activeKeys + noteName newActiveKeys.add(noteName)
println("startNote $noteName $frequency")
if (synth.isActive()) { if (synth.isActive()) {
// Легато - плавный переход на новую ноту // Легато - плавный переход на новую ноту
synth.changeNote(frequency) synth.changeNote(frequency)
@ -86,21 +85,16 @@ fun PianoKeyboard(
// ОТПУСКАНИЕ - когда указатель отпущен // ОТПУСКАНИЕ - когда указатель отпущен
!change.pressed -> { !change.pressed -> {
val releasedNoteName = activePointers.remove(pointerId) val releasedNote = activePointers.remove(pointerId)
releasedNoteName?.let { noteName -> releasedNote?.let { (noteName, _) ->
activeKeys = activeKeys - noteName
if (activePointers.isEmpty()) { if (activePointers.isEmpty()) {
// Все клавиши отпущены - останавливаем ноту // Все клавиши отпущены - останавливаем ноту
synth.stopNote() synth.stopNote()
} else { } else {
// Есть другие активные клавиши - переключаемся на одну из них // Есть другие активные клавиши - переключаемся на одну из них
val remainingNoteName = activePointers.values.firstOrNull() val remainingNote = activePointers.values.firstOrNull()
remainingNoteName?.let { name -> remainingNote?.let {
val remainingNote = availableNotes.find { it.first == name } synth.changeNote(it.second)
remainingNote?.let {
synth.changeNote(it.second)
}
} }
} }
} }
@ -109,16 +103,12 @@ fun PianoKeyboard(
// ПЕРЕМЕЩЕНИЕ - когда указатель переместился // ПЕРЕМЕЩЕНИЕ - когда указатель переместился
change.positionChanged() -> { change.positionChanged() -> {
key?.let { (newNoteName, newFrequency) -> key?.let { (newNoteName, newFrequency) ->
val currentNoteName = activePointers[pointerId] val currentNote = activePointers[pointerId]
// Если указатель переместился на другую клавишу // Если указатель переместился на другую клавишу
if (currentNoteName != newNoteName) { if (currentNote?.first != newNoteName) {
// Обновляем активные клавиши // Обновляем активную ноту для этого указателя
currentNoteName?.let { activePointers[pointerId] = newNoteName to newFrequency
activeKeys = activeKeys - it
}
activeKeys = activeKeys + newNoteName
activePointers[pointerId] = newNoteName
// Плавно переключаем ноту // Плавно переключаем ноту
if (synth.isActive()) { if (synth.isActive()) {
@ -132,6 +122,17 @@ fun PianoKeyboard(
// Всегда сообщаем, что обработали событие // Всегда сообщаем, что обработали событие
change.consume() change.consume()
} }
// ОБНОВЛЯЕМ АКТИВНЫЕ КЛАВИШИ на основе текущего состояния всех указателей
// Это гарантирует, что визуальное состояние всегда соответствует звуковому
activePointers.values.forEach { (noteName, _) ->
newActiveKeys.add(noteName)
}
// Применяем обновление активных клавиш
if (activeKeys != newActiveKeys) {
activeKeys = newActiveKeys
}
} }
} }
} }
@ -155,7 +156,7 @@ fun PianoKeyboard(
} }
} }
// Вспомогательные функции остаются без изменений: // Остальные вспомогательные функции остаются без изменений:
private fun findKeyAtOffset( private fun findKeyAtOffset(
offset: Offset, offset: Offset,
@ -183,7 +184,7 @@ private fun findKeyAtOffsetHorizontal(
blackKeys.forEachIndexed { index, blackKey -> blackKeys.forEachIndexed { index, blackKey ->
val blackKeyX = (getWhiteKeyIndexForBlackKey(blackKey.first, whiteKeys) * whiteKeyWidth) - val blackKeyX = (getWhiteKeyIndexForBlackKey(blackKey.first, whiteKeys) * whiteKeyWidth) -
(whiteKeyWidth * 0.25f) (whiteKeyWidth * 0.25f)
val blackKeyRect = Rect( val blackKeyRect = androidx.compose.ui.geometry.Rect(
left = blackKeyX, left = blackKeyX,
top = 0f, top = 0f,
right = blackKeyX + whiteKeyWidth * 0.5f, right = blackKeyX + whiteKeyWidth * 0.5f,
@ -196,7 +197,7 @@ private fun findKeyAtOffsetHorizontal(
// Затем проверяем белые клавиши // Затем проверяем белые клавиши
whiteKeys.forEachIndexed { index, whiteKey -> whiteKeys.forEachIndexed { index, whiteKey ->
val whiteKeyRect = Rect( val whiteKeyRect = androidx.compose.ui.geometry.Rect(
left = index * whiteKeyWidth, left = index * whiteKeyWidth,
top = 0f, top = 0f,
right = (index + 1) * whiteKeyWidth, right = (index + 1) * whiteKeyWidth,
@ -222,7 +223,7 @@ private fun findKeyAtOffsetVertical(
blackKeys.forEachIndexed { index, blackKey -> blackKeys.forEachIndexed { index, blackKey ->
val blackKeyY = (getWhiteKeyIndexForBlackKey(blackKey.first, whiteKeys) * whiteKeyHeight) - val blackKeyY = (getWhiteKeyIndexForBlackKey(blackKey.first, whiteKeys) * whiteKeyHeight) -
(whiteKeyHeight * 0.25f) (whiteKeyHeight * 0.25f)
val blackKeyRect = Rect( val blackKeyRect = androidx.compose.ui.geometry.Rect(
left = size.width * 0.4f, left = size.width * 0.4f,
top = blackKeyY, top = blackKeyY,
right = size.width, right = size.width,
@ -235,7 +236,7 @@ private fun findKeyAtOffsetVertical(
// Затем проверяем белые клавиши // Затем проверяем белые клавиши
whiteKeys.forEachIndexed { index, whiteKey -> whiteKeys.forEachIndexed { index, whiteKey ->
val whiteKeyRect = Rect( val whiteKeyRect = androidx.compose.ui.geometry.Rect(
left = 0f, left = 0f,
top = index * whiteKeyHeight, top = index * whiteKeyHeight,
right = size.width, right = size.width,

View File

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
android:height="108dp"
android:width="108dp"
android:viewportHeight="108"
android:viewportWidth="108"
xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z"/>
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
</vector>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher2_background"/>
<foreground android:drawable="@mipmap/ic_launcher2_foreground"/>
</adaptive-icon>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher2_background"/>
<foreground android:drawable="@mipmap/ic_launcher2_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -1,6 +1,6 @@
[versions] [versions]
agp = "8.13.0" agp = "8.13.0"
kotlin = "2.0.21" kotlin = "2.2.20"
coreKtx = "1.10.1" coreKtx = "1.10.1"
junit = "4.13.2" junit = "4.13.2"
junitVersion = "1.1.5" junitVersion = "1.1.5"