Compare commits
No commits in common. "a78093c6b8337bd0a387ad8f15385273af29afaa" and "8b6826170194b471c43c9103d0bae32eda93f5e6" have entirely different histories.
a78093c6b8
...
8b68261701
1
.gitignore
vendored
@ -9,4 +9,3 @@
|
|||||||
.externalNativeBuild
|
.externalNativeBuild
|
||||||
.cxx
|
.cxx
|
||||||
local.properties
|
local.properties
|
||||||
/app/release/
|
|
||||||
|
@ -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_launcher2"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher2_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.Karabass">
|
android:theme="@style/Theme.Karabass">
|
||||||
<activity
|
<activity
|
||||||
|
Before Width: | Height: | Size: 402 KiB |
@ -1,4 +1,5 @@
|
|||||||
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
|
||||||
@ -39,7 +40,6 @@ 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,24 +55,23 @@ 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()
|
private val attackSamples = (0.02 * SAMPLE_RATE).toInt() // Увеличил до 20ms для более плавного старта
|
||||||
private val releaseSamples = (0.2 * SAMPLE_RATE).toInt()
|
private val releaseSamples = (0.2 * SAMPLE_RATE).toInt() // 200ms релиз
|
||||||
|
|
||||||
// Минимальный буфер для 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(): Boolean {
|
private fun initializeAudioTrack() {
|
||||||
synchronized(lock) {
|
synchronized(lock) {
|
||||||
if (isReleased) return false
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Освобождаем старый AudioTrack если есть
|
// Освобождаем старый AudioTrack если есть
|
||||||
audioTrack?.let { track ->
|
audioTrack?.let { track ->
|
||||||
@ -82,11 +81,11 @@ class AdvancedTubaSynth(private val context: Context) {
|
|||||||
}
|
}
|
||||||
track.release()
|
track.release()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Игнорируем ошибки при освобождении
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val newTrack = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
audioTrack = 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)
|
||||||
@ -101,7 +100,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")
|
||||||
@ -110,29 +109,22 @@ 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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newTrack.state == AudioTrack.STATE_INITIALIZED) {
|
audioTrack?.play()
|
||||||
audioTrack = newTrack
|
|
||||||
newTrack.play()
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
newTrack.release()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
return false
|
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
|
||||||
|
initializeAudioTrack()
|
||||||
|
}, 100)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startNote(frequency: Float) {
|
fun startNote(frequency: Float) {
|
||||||
if (isReleased) return
|
|
||||||
|
|
||||||
synchronized(lock) {
|
synchronized(lock) {
|
||||||
targetFrequency = frequency
|
targetFrequency = frequency
|
||||||
|
|
||||||
@ -143,6 +135,7 @@ 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
|
||||||
|
|
||||||
// Сбрасываем фазу при начале новой ноты для предотвращения щелчков
|
// Сбрасываем фазу при начале новой ноты для предотвращения щелчков
|
||||||
@ -159,8 +152,6 @@ 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
|
||||||
@ -171,39 +162,29 @@ 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 || isReleased) return
|
if (isPlaying) 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) {
|
||||||
if (!initializeAudioTrack()) {
|
initializeAudioTrack()
|
||||||
isPlaying = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
currentThread = Thread {
|
currentThread = Thread {
|
||||||
val buffer = ShortArray(bufferSize)
|
val buffer = ShortArray(bufferSize)
|
||||||
var currentAudioTrack: AudioTrack? = null
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Получаем ссылку на AudioTrack один раз в начале
|
while (!shouldStop && isPlaying) {
|
||||||
synchronized(lock) {
|
|
||||||
currentAudioTrack = audioTrack
|
|
||||||
}
|
|
||||||
|
|
||||||
while (!shouldStop && isPlaying && !isReleased) {
|
|
||||||
// Заполняем буфер
|
// Заполняем буфер
|
||||||
var allSamplesZero = true
|
var allSamplesZero = true
|
||||||
for (i in buffer.indices) {
|
for (i in buffer.indices) {
|
||||||
@ -215,7 +196,7 @@ class AdvancedTubaSynth(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Безопасная запись в AudioTrack
|
// Безопасная запись в AudioTrack
|
||||||
val track = currentAudioTrack
|
val track = audioTrack
|
||||||
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)
|
||||||
@ -223,29 +204,18 @@ 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 && !isReleased) {
|
if (!shouldStop) {
|
||||||
// Пытаемся восстановить AudioTrack
|
e.printStackTrace()
|
||||||
synchronized(lock) {
|
android.os.Handler(android.os.Looper.getMainLooper()).post {
|
||||||
if (initializeAudioTrack()) {
|
initializeAudioTrack()
|
||||||
currentAudioTrack = audioTrack
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// AudioTrack не готов
|
|
||||||
if (!shouldStop && !isReleased) {
|
|
||||||
synchronized(lock) {
|
|
||||||
if (initializeAudioTrack()) {
|
|
||||||
currentAudioTrack = audioTrack
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -255,11 +225,12 @@ class AdvancedTubaSynth(private val context: Context) {
|
|||||||
// 1. Огибающая завершила релиз
|
// 1. Огибающая завершила релиз
|
||||||
// 2. Мы записали несколько буферов тишины для гарантии
|
// 2. Мы записали несколько буферов тишины для гарантии
|
||||||
// 3. Нет активной ноты
|
// 3. Нет активной ноты
|
||||||
if (silenceBuffersWritten >= requiredSilenceBuffers) {
|
if (lastBufferWritten) {
|
||||||
synchronized(lock) {
|
synchronized(lock) {
|
||||||
if (!isNoteOn && envelopeState == 0) {
|
if (!isNoteOn && envelopeState == 0) {
|
||||||
|
// Даем дополнительное время на вывод последних буферов
|
||||||
|
Thread.sleep(50)
|
||||||
isPlaying = false
|
isPlaying = false
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -276,8 +247,6 @@ 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
|
||||||
@ -349,13 +318,21 @@ 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 !isReleased && (isNoteOn || envelopeState != 0)
|
return isNoteOn || envelopeState != 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -366,14 +343,20 @@ 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(500) // Увеличил время ожидания
|
thread.join(200)
|
||||||
} catch (e: InterruptedException) {
|
} catch (e: InterruptedException) {
|
||||||
thread.interrupt()
|
thread.interrupt()
|
||||||
}
|
}
|
||||||
@ -392,7 +375,7 @@ class AdvancedTubaSynth(private val context: Context) {
|
|||||||
}
|
}
|
||||||
track.release()
|
track.release()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Игнорируем ошибки при освобождении
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
audioTrack = null
|
audioTrack = null
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
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
|
||||||
@ -42,16 +44,14 @@ fun PianoKeyboard(
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.pointerInput(synth, whiteKeys, blackKeys, isHorizontal, availableNotes) {
|
.pointerInput(synth, whiteKeys, blackKeys, isHorizontal, availableNotes) {
|
||||||
awaitPointerEventScope {
|
awaitPointerEventScope {
|
||||||
// Карта для отслеживания активных указателей и их текущих нот
|
// Карта для отслеживания активных указателей и их нот
|
||||||
val activePointers = mutableMapOf<PointerId, Pair<String, Float>>()
|
val activePointers = mutableMapOf<PointerId, String>()
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
val event = awaitPointerEvent()
|
val event = awaitPointerEvent()
|
||||||
|
|
||||||
// Временный набор для обновления активных клавиш
|
// Обрабатываем все изменения в событии
|
||||||
val newActiveKeys = mutableSetOf<String>()
|
for (change in event.changes) {
|
||||||
|
|
||||||
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,9 +70,10 @@ fun PianoKeyboard(
|
|||||||
change.pressed -> {
|
change.pressed -> {
|
||||||
key?.let { (noteName, frequency) ->
|
key?.let { (noteName, frequency) ->
|
||||||
// Запоминаем связь указатель-нота
|
// Запоминаем связь указатель-нота
|
||||||
activePointers[pointerId] = noteName to frequency
|
activePointers[pointerId] = noteName
|
||||||
newActiveKeys.add(noteName)
|
activeKeys = activeKeys + noteName
|
||||||
|
|
||||||
|
println("startNote $noteName $frequency")
|
||||||
if (synth.isActive()) {
|
if (synth.isActive()) {
|
||||||
// Легато - плавный переход на новую ноту
|
// Легато - плавный переход на новую ноту
|
||||||
synth.changeNote(frequency)
|
synth.changeNote(frequency)
|
||||||
@ -85,30 +86,39 @@ fun PianoKeyboard(
|
|||||||
|
|
||||||
// ОТПУСКАНИЕ - когда указатель отпущен
|
// ОТПУСКАНИЕ - когда указатель отпущен
|
||||||
!change.pressed -> {
|
!change.pressed -> {
|
||||||
val releasedNote = activePointers.remove(pointerId)
|
val releasedNoteName = activePointers.remove(pointerId)
|
||||||
releasedNote?.let { (noteName, _) ->
|
releasedNoteName?.let { noteName ->
|
||||||
|
activeKeys = activeKeys - noteName
|
||||||
|
|
||||||
if (activePointers.isEmpty()) {
|
if (activePointers.isEmpty()) {
|
||||||
// Все клавиши отпущены - останавливаем ноту
|
// Все клавиши отпущены - останавливаем ноту
|
||||||
synth.stopNote()
|
synth.stopNote()
|
||||||
} else {
|
} else {
|
||||||
// Есть другие активные клавиши - переключаемся на одну из них
|
// Есть другие активные клавиши - переключаемся на одну из них
|
||||||
val remainingNote = activePointers.values.firstOrNull()
|
val remainingNoteName = activePointers.values.firstOrNull()
|
||||||
|
remainingNoteName?.let { name ->
|
||||||
|
val remainingNote = availableNotes.find { it.first == name }
|
||||||
remainingNote?.let {
|
remainingNote?.let {
|
||||||
synth.changeNote(it.second)
|
synth.changeNote(it.second)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ПЕРЕМЕЩЕНИЕ - когда указатель переместился
|
// ПЕРЕМЕЩЕНИЕ - когда указатель переместился
|
||||||
change.positionChanged() -> {
|
change.positionChanged() -> {
|
||||||
key?.let { (newNoteName, newFrequency) ->
|
key?.let { (newNoteName, newFrequency) ->
|
||||||
val currentNote = activePointers[pointerId]
|
val currentNoteName = activePointers[pointerId]
|
||||||
|
|
||||||
// Если указатель переместился на другую клавишу
|
// Если указатель переместился на другую клавишу
|
||||||
if (currentNote?.first != newNoteName) {
|
if (currentNoteName != newNoteName) {
|
||||||
// Обновляем активную ноту для этого указателя
|
// Обновляем активные клавиши
|
||||||
activePointers[pointerId] = newNoteName to newFrequency
|
currentNoteName?.let {
|
||||||
|
activeKeys = activeKeys - it
|
||||||
|
}
|
||||||
|
activeKeys = activeKeys + newNoteName
|
||||||
|
activePointers[pointerId] = newNoteName
|
||||||
|
|
||||||
// Плавно переключаем ноту
|
// Плавно переключаем ноту
|
||||||
if (synth.isActive()) {
|
if (synth.isActive()) {
|
||||||
@ -122,17 +132,6 @@ fun PianoKeyboard(
|
|||||||
// Всегда сообщаем, что обработали событие
|
// Всегда сообщаем, что обработали событие
|
||||||
change.consume()
|
change.consume()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ОБНОВЛЯЕМ АКТИВНЫЕ КЛАВИШИ на основе текущего состояния всех указателей
|
|
||||||
// Это гарантирует, что визуальное состояние всегда соответствует звуковому
|
|
||||||
activePointers.values.forEach { (noteName, _) ->
|
|
||||||
newActiveKeys.add(noteName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Применяем обновление активных клавиш
|
|
||||||
if (activeKeys != newActiveKeys) {
|
|
||||||
activeKeys = newActiveKeys
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -156,7 +155,7 @@ fun PianoKeyboard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Остальные вспомогательные функции остаются без изменений:
|
// Вспомогательные функции остаются без изменений:
|
||||||
|
|
||||||
private fun findKeyAtOffset(
|
private fun findKeyAtOffset(
|
||||||
offset: Offset,
|
offset: Offset,
|
||||||
@ -184,7 +183,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 = androidx.compose.ui.geometry.Rect(
|
val blackKeyRect = Rect(
|
||||||
left = blackKeyX,
|
left = blackKeyX,
|
||||||
top = 0f,
|
top = 0f,
|
||||||
right = blackKeyX + whiteKeyWidth * 0.5f,
|
right = blackKeyX + whiteKeyWidth * 0.5f,
|
||||||
@ -197,7 +196,7 @@ private fun findKeyAtOffsetHorizontal(
|
|||||||
|
|
||||||
// Затем проверяем белые клавиши
|
// Затем проверяем белые клавиши
|
||||||
whiteKeys.forEachIndexed { index, whiteKey ->
|
whiteKeys.forEachIndexed { index, whiteKey ->
|
||||||
val whiteKeyRect = androidx.compose.ui.geometry.Rect(
|
val whiteKeyRect = Rect(
|
||||||
left = index * whiteKeyWidth,
|
left = index * whiteKeyWidth,
|
||||||
top = 0f,
|
top = 0f,
|
||||||
right = (index + 1) * whiteKeyWidth,
|
right = (index + 1) * whiteKeyWidth,
|
||||||
@ -223,7 +222,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 = androidx.compose.ui.geometry.Rect(
|
val blackKeyRect = Rect(
|
||||||
left = size.width * 0.4f,
|
left = size.width * 0.4f,
|
||||||
top = blackKeyY,
|
top = blackKeyY,
|
||||||
right = size.width,
|
right = size.width,
|
||||||
@ -236,7 +235,7 @@ private fun findKeyAtOffsetVertical(
|
|||||||
|
|
||||||
// Затем проверяем белые клавиши
|
// Затем проверяем белые клавиши
|
||||||
whiteKeys.forEachIndexed { index, whiteKey ->
|
whiteKeys.forEachIndexed { index, whiteKey ->
|
||||||
val whiteKeyRect = androidx.compose.ui.geometry.Rect(
|
val whiteKeyRect = Rect(
|
||||||
left = 0f,
|
left = 0f,
|
||||||
top = index * whiteKeyHeight,
|
top = index * whiteKeyHeight,
|
||||||
right = size.width,
|
right = size.width,
|
||||||
|
@ -1,74 +0,0 @@
|
|||||||
<?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>
|
|
@ -1,5 +0,0 @@
|
|||||||
<?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>
|
|
@ -1,5 +0,0 @@
|
|||||||
<?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>
|
|
Before Width: | Height: | Size: 6.0 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 9.4 KiB |
Before Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 95 KiB |
Before Width: | Height: | Size: 38 KiB |
@ -1,6 +1,6 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "8.13.0"
|
agp = "8.13.0"
|
||||||
kotlin = "2.2.20"
|
kotlin = "2.0.21"
|
||||||
coreKtx = "1.10.1"
|
coreKtx = "1.10.1"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
junitVersion = "1.1.5"
|
junitVersion = "1.1.5"
|
||||||
|