less clicks on note end/start

This commit is contained in:
Sergey Chernov 2025-10-13 13:21:50 +04:00
parent ab0c5147e6
commit 8b68261701

View File

@ -55,11 +55,16 @@ 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.01 * SAMPLE_RATE).toInt() // 10ms атака private val attackSamples = (0.02 * SAMPLE_RATE).toInt() // Увеличил до 20ms для более плавного старта
private val releaseSamples = (0.15 * SAMPLE_RATE).toInt() // 150ms релиз private val releaseSamples = (0.2 * SAMPLE_RATE).toInt() // 200ms релиз
// Минимальный буфер для low-latency // Минимальный буфер для low-latency
private val bufferSize = 512 private val bufferSize = 1024 // Увеличил буфер для стабильности
// Для управления выводом
private var lastBufferWritten = false
private var silenceBuffersWritten = 0
private val requiredSilenceBuffers = 3 // Дополнительные буферы тишины после затухания
init { init {
initializeAudioTrack() initializeAudioTrack()
@ -68,6 +73,18 @@ class AdvancedTubaSynth(private val context: Context) {
private fun initializeAudioTrack() { private fun initializeAudioTrack() {
synchronized(lock) { synchronized(lock) {
try { 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) { 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)
@ -83,7 +100,7 @@ class AdvancedTubaSynth(private val context: Context) {
AudioTrack.Builder() AudioTrack.Builder()
.setAudioAttributes(attributes) .setAudioAttributes(attributes)
.setAudioFormat(format) .setAudioFormat(format)
.setBufferSizeInBytes(bufferSize * 2) .setBufferSizeInBytes(bufferSize * 4) // Увеличил размер буфера
.build() .build()
} else { } else {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
@ -92,7 +109,7 @@ 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 * 2, bufferSize * 4, // Увеличил размер буфера
AudioTrack.MODE_STREAM AudioTrack.MODE_STREAM
) )
} }
@ -118,6 +135,11 @@ class AdvancedTubaSynth(private val context: Context) {
envelopeSamples = 0 envelopeSamples = 0
envelopeValue = 0.0 envelopeValue = 0.0
currentFrequency = frequency currentFrequency = frequency
lastBufferWritten = false
silenceBuffersWritten = 0
// Сбрасываем фазу при начале новой ноты для предотвращения щелчков
phase = 0.0
if (!isPlaying) { if (!isPlaying) {
startAudioGeneration() startAudioGeneration()
@ -150,6 +172,8 @@ class AdvancedTubaSynth(private val context: Context) {
shouldStop = false shouldStop = false
isPlaying = true isPlaying = true
lastBufferWritten = false
silenceBuffersWritten = 0
val track = audioTrack val track = audioTrack
if (track == null || track.state != AudioTrack.STATE_INITIALIZED) { 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) { 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)
// Если это последний буфер с данными, запоминаем это
if (allSamplesZero && envelopeState == 0) {
silenceBuffersWritten++
if (silenceBuffersWritten >= requiredSilenceBuffers) {
lastBufferWritten = true
}
} else {
silenceBuffersWritten = 0
lastBufferWritten = false
}
} catch (e: Exception) { } catch (e: Exception) {
if (!shouldStop) { if (!shouldStop) {
e.printStackTrace() e.printStackTrace()
@ -186,10 +221,15 @@ class AdvancedTubaSynth(private val context: Context) {
} }
} }
// Останавливаем генерацию только если огибающая завершила релиз // Останавливаем генерацию только после того как:
if (allSamplesZero && envelopeState == 0) { // 1. Огибающая завершила релиз
// 2. Мы записали несколько буферов тишины для гарантии
// 3. Нет активной ноты
if (lastBufferWritten) {
synchronized(lock) { synchronized(lock) {
if (!isNoteOn) { if (!isNoteOn && envelopeState == 0) {
// Даем дополнительное время на вывод последних буферов
Thread.sleep(50)
isPlaying = false isPlaying = false
} }
} }
@ -233,10 +273,10 @@ class AdvancedTubaSynth(private val context: Context) {
envelopeSamples++ envelopeSamples++
if (envelopeSamples >= attackSamples) { if (envelopeSamples >= attackSamples) {
envelopeValue = 1.0 envelopeValue = 1.0
// Остаемся в атаке - переход к релизу только при stopNote()
} else { } else {
// Плавная атака // Квадратичная атака для более плавного начала
envelopeValue = envelopeSamples.toDouble() / attackSamples val progress = envelopeSamples.toDouble() / attackSamples
envelopeValue = progress * progress
} }
} }
2 -> { // Релиз 2 -> { // Релиз
@ -245,11 +285,12 @@ class AdvancedTubaSynth(private val context: Context) {
envelopeValue = 0.0 envelopeValue = 0.0
envelopeState = 0 // idle envelopeState = 0 // idle
} else { } 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 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 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 {
@ -297,9 +346,9 @@ class AdvancedTubaSynth(private val context: Context) {
shouldStop = true shouldStop = true
isPlaying = false isPlaying = false
// Даем время на завершение релиза // Даем время на завершение релиза и вывод всех буферов
try { try {
Thread.sleep(200) Thread.sleep(300)
} catch (e: InterruptedException) { } catch (e: InterruptedException) {
// Игнорируем // Игнорируем
} }
@ -307,7 +356,7 @@ class AdvancedTubaSynth(private val context: Context) {
// Безопасная остановка потока // Безопасная остановка потока
currentThread?.let { thread -> currentThread?.let { thread ->
try { try {
thread.join(100) thread.join(200)
} catch (e: InterruptedException) { } catch (e: InterruptedException) {
thread.interrupt() thread.interrupt()
} }
@ -317,6 +366,10 @@ class AdvancedTubaSynth(private val context: Context) {
// Безопасное освобождение AudioTrack // Безопасное освобождение AudioTrack
audioTrack?.let { track -> audioTrack?.let { track ->
try { try {
// Плавно уменьшаем громкость перед остановкой
track.setVolume(0.1f)
Thread.sleep(50)
if (track.playState == AudioTrack.PLAYSTATE_PLAYING) { if (track.playState == AudioTrack.PLAYSTATE_PLAYING) {
track.stop() track.stop()
} }