From 8b6826170194b471c43c9103d0bae32eda93f5e6 Mon Sep 17 00:00:00 2001 From: sergeych Date: Mon, 13 Oct 2025 13:21:50 +0400 Subject: [PATCH] less clicks on note end/start --- .../sergeych/karabass/AdvancedTubaSynth.kt | 89 +++++++++++++++---- 1 file changed, 71 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/net/sergeych/karabass/AdvancedTubaSynth.kt b/app/src/main/java/net/sergeych/karabass/AdvancedTubaSynth.kt index e292f4e..1b31cf1 100644 --- a/app/src/main/java/net/sergeych/karabass/AdvancedTubaSynth.kt +++ b/app/src/main/java/net/sergeych/karabass/AdvancedTubaSynth.kt @@ -55,11 +55,16 @@ class AdvancedTubaSynth(private val context: Context) { private var envelopeState = 0 // 0: idle, 1: attack, 2: release // Параметры огибающей - private val attackSamples = (0.01 * SAMPLE_RATE).toInt() // 10ms атака - private val releaseSamples = (0.15 * SAMPLE_RATE).toInt() // 150ms релиз + private val attackSamples = (0.02 * SAMPLE_RATE).toInt() // Увеличил до 20ms для более плавного старта + private val releaseSamples = (0.2 * SAMPLE_RATE).toInt() // 200ms релиз // Минимальный буфер для low-latency - private val bufferSize = 512 + private val bufferSize = 1024 // Увеличил буфер для стабильности + + // Для управления выводом + private var lastBufferWritten = false + private var silenceBuffersWritten = 0 + private val requiredSilenceBuffers = 3 // Дополнительные буферы тишины после затухания init { initializeAudioTrack() @@ -68,6 +73,18 @@ class AdvancedTubaSynth(private val context: Context) { private fun initializeAudioTrack() { synchronized(lock) { try { + // Освобождаем старый AudioTrack если есть + audioTrack?.let { track -> + try { + if (track.playState == AudioTrack.PLAYSTATE_PLAYING) { + track.stop() + } + track.release() + } catch (e: Exception) { + e.printStackTrace() + } + } + audioTrack = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { val attributes = AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) @@ -83,7 +100,7 @@ class AdvancedTubaSynth(private val context: Context) { AudioTrack.Builder() .setAudioAttributes(attributes) .setAudioFormat(format) - .setBufferSizeInBytes(bufferSize * 2) + .setBufferSizeInBytes(bufferSize * 4) // Увеличил размер буфера .build() } else { @Suppress("DEPRECATION") @@ -92,7 +109,7 @@ class AdvancedTubaSynth(private val context: Context) { SAMPLE_RATE, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT, - bufferSize * 2, + bufferSize * 4, // Увеличил размер буфера AudioTrack.MODE_STREAM ) } @@ -118,6 +135,11 @@ class AdvancedTubaSynth(private val context: Context) { envelopeSamples = 0 envelopeValue = 0.0 currentFrequency = frequency + lastBufferWritten = false + silenceBuffersWritten = 0 + + // Сбрасываем фазу при начале новой ноты для предотвращения щелчков + phase = 0.0 if (!isPlaying) { startAudioGeneration() @@ -150,6 +172,8 @@ class AdvancedTubaSynth(private val context: Context) { shouldStop = false isPlaying = true + lastBufferWritten = false + silenceBuffersWritten = 0 val track = audioTrack if (track == null || track.state != AudioTrack.STATE_INITIALIZED) { @@ -176,6 +200,17 @@ class AdvancedTubaSynth(private val context: Context) { if (track?.state == AudioTrack.STATE_INITIALIZED && track.playState == AudioTrack.PLAYSTATE_PLAYING) { try { track.write(buffer, 0, buffer.size) + + // Если это последний буфер с данными, запоминаем это + if (allSamplesZero && envelopeState == 0) { + silenceBuffersWritten++ + if (silenceBuffersWritten >= requiredSilenceBuffers) { + lastBufferWritten = true + } + } else { + silenceBuffersWritten = 0 + lastBufferWritten = false + } } catch (e: Exception) { if (!shouldStop) { e.printStackTrace() @@ -186,10 +221,15 @@ class AdvancedTubaSynth(private val context: Context) { } } - // Останавливаем генерацию только если огибающая завершила релиз - if (allSamplesZero && envelopeState == 0) { + // Останавливаем генерацию только после того как: + // 1. Огибающая завершила релиз + // 2. Мы записали несколько буферов тишины для гарантии + // 3. Нет активной ноты + if (lastBufferWritten) { synchronized(lock) { - if (!isNoteOn) { + if (!isNoteOn && envelopeState == 0) { + // Даем дополнительное время на вывод последних буферов + Thread.sleep(50) isPlaying = false } } @@ -233,10 +273,10 @@ class AdvancedTubaSynth(private val context: Context) { envelopeSamples++ if (envelopeSamples >= attackSamples) { envelopeValue = 1.0 - // Остаемся в атаке - переход к релизу только при stopNote() } else { - // Плавная атака - envelopeValue = envelopeSamples.toDouble() / attackSamples + // Квадратичная атака для более плавного начала + val progress = envelopeSamples.toDouble() / attackSamples + envelopeValue = progress * progress } } 2 -> { // Релиз @@ -245,11 +285,12 @@ class AdvancedTubaSynth(private val context: Context) { envelopeValue = 0.0 envelopeState = 0 // idle } else { - // Плавный релиз - envelopeValue = 1.0 - (envelopeSamples.toDouble() / releaseSamples) + // Квадратичный релиз для более плавного затухания + val progress = envelopeSamples.toDouble() / releaseSamples + envelopeValue = (1.0 - progress) * (1.0 - progress) // Гарантируем плавное затухание в конце - if (envelopeValue < 0.01) { + if (envelopeValue < 0.001) { envelopeValue = 0.0 } } @@ -277,8 +318,16 @@ class AdvancedTubaSynth(private val context: Context) { // Смешиваем с весами, характерными для тубы 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 { @@ -297,9 +346,9 @@ class AdvancedTubaSynth(private val context: Context) { shouldStop = true isPlaying = false - // Даем время на завершение релиза + // Даем время на завершение релиза и вывод всех буферов try { - Thread.sleep(200) + Thread.sleep(300) } catch (e: InterruptedException) { // Игнорируем } @@ -307,7 +356,7 @@ class AdvancedTubaSynth(private val context: Context) { // Безопасная остановка потока currentThread?.let { thread -> try { - thread.join(100) + thread.join(200) } catch (e: InterruptedException) { thread.interrupt() } @@ -317,6 +366,10 @@ class AdvancedTubaSynth(private val context: Context) { // Безопасное освобождение AudioTrack audioTrack?.let { track -> try { + // Плавно уменьшаем громкость перед остановкой + track.setVolume(0.1f) + Thread.sleep(50) + if (track.playState == AudioTrack.PLAYSTATE_PLAYING) { track.stop() }