less clicks on note end/start

This commit is contained in:
Sergey Chernov 2025-10-13 12:24:46 +04:00
parent fc12c9d932
commit c87585693f

View File

@ -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
@ -6,13 +7,11 @@ import android.media.AudioManager
import android.media.AudioTrack import android.media.AudioTrack
import android.os.Build import android.os.Build
import kotlin.math.* import kotlin.math.*
import kotlin.random.Random
class AdvancedTubaSynth(private val context: Context) { class AdvancedTubaSynth(private val context: Context) {
companion object { companion object {
const val SAMPLE_RATE = 44100 const val SAMPLE_RATE = 44100
const val CUTOFF_FREQ = 800.0
// Полный звуковой ряд тубы с полутонами (от C1 до B5) // Полный звуковой ряд тубы с полутонами (от C1 до B5)
val TUBA_NOTES = listOf( val TUBA_NOTES = listOf(
@ -44,30 +43,27 @@ class AdvancedTubaSynth(private val context: Context) {
private var currentThread: Thread? = null private var currentThread: Thread? = null
// Синхронизированные переменные для потокобезопасности // Синхронизированные переменные для потокобезопасности
private val lock = Object() private val lock = Any()
private var currentFrequency = 0f private var currentFrequency = 0f
private var targetFrequency = 0f private var targetFrequency = 0f
private var isNoteOn = false private var isNoteOn = false
private var currentAmplitude = 0.0 private var currentAmplitude = 0.0
private var samplesSinceNoteOn = 0
private var samplesSinceNoteOff = 0
private var phase = 0.0 private var phase = 0.0
// Явное состояние огибающей // Простая и надежная огибающая
private enum class EnvelopeState { IDLE, ATTACK, DECAY, SUSTAIN, RELEASE } private var envelopeValue = 0.0
private var envelopeState = EnvelopeState.IDLE private var envelopeState = 0 // 0: idle, 1: attack, 2: sustain, 3: release
private var envelopeCounter = 0
// Оптимизированные параметры для уменьшения задержки // Параметры огибающей
private val attackTime = (0.005 * SAMPLE_RATE).toInt() // 5ms атака private val attackSamples = (0.01 * SAMPLE_RATE).toInt() // 10ms атака
private val decayTime = (0.03 * SAMPLE_RATE).toInt() // 30ms спад private val releaseSamples = (0.1 * SAMPLE_RATE).toInt() // 100ms релиз
private val releaseTime = (0.05 * SAMPLE_RATE).toInt() // 50ms релиз
private val sustainLevel = 0.8 private val sustainLevel = 0.8
// Минимальный буфер для low-latency // Минимальный буфер для low-latency
private val bufferSize = 512 // ~11ms latency private val bufferSize = 512
init { init {
// Создаем AudioTrack один раз при инициализации
initializeAudioTrack() initializeAudioTrack()
} }
@ -106,7 +102,6 @@ class AdvancedTubaSynth(private val context: Context) {
audioTrack?.play() audioTrack?.play()
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
// Пытаемся восстановить AudioTrack через некоторое время
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({ android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
initializeAudioTrack() initializeAudioTrack()
}, 100) }, 100)
@ -119,18 +114,18 @@ class AdvancedTubaSynth(private val context: Context) {
targetFrequency = frequency targetFrequency = frequency
if (!isNoteOn) { if (!isNoteOn) {
// Новая нота - сбрасываем все счетчики // Новая нота - начинаем с атаки
isNoteOn = true isNoteOn = true
samplesSinceNoteOn = 0 envelopeState = 1 // attack
samplesSinceNoteOff = 0 envelopeCounter = 0
envelopeState = EnvelopeState.ATTACK envelopeValue = 0.0
currentFrequency = frequency currentFrequency = frequency
if (!isPlaying) { if (!isPlaying) {
startAudioGeneration() startAudioGeneration()
} }
} else { } else {
// Легато - быстрый переход // Легато - просто меняем частоту
targetFrequency = frequency targetFrequency = frequency
} }
} }
@ -140,10 +135,9 @@ class AdvancedTubaSynth(private val context: Context) {
synchronized(lock) { synchronized(lock) {
if (isNoteOn) { if (isNoteOn) {
isNoteOn = false isNoteOn = false
samplesSinceNoteOff = 0 if (envelopeState != 0) {
// Переходим в состояние релиза только если мы не в IDLE envelopeState = 3 // release
if (envelopeState != EnvelopeState.IDLE) { envelopeCounter = 0
envelopeState = EnvelopeState.RELEASE
} }
} }
} }
@ -161,7 +155,6 @@ class AdvancedTubaSynth(private val context: Context) {
shouldStop = false shouldStop = false
isPlaying = true isPlaying = true
// Проверяем, что AudioTrack существует и готов
val track = audioTrack val track = audioTrack
if (track == null || track.state != AudioTrack.STATE_INITIALIZED) { if (track == null || track.state != AudioTrack.STATE_INITIALIZED) {
initializeAudioTrack() initializeAudioTrack()
@ -169,13 +162,18 @@ class AdvancedTubaSynth(private val context: Context) {
currentThread = Thread { currentThread = Thread {
val buffer = ShortArray(bufferSize) val buffer = ShortArray(bufferSize)
var continueGeneration = true
try { try {
while (!shouldStop && isPlaying) { while (!shouldStop && isPlaying && continueGeneration) {
// Заполняем буфер // Заполняем буфер
var allSamplesZero = true
for (i in buffer.indices) { for (i in buffer.indices) {
val sample = generateSample() val sample = generateSample()
buffer[i] = (sample.coerceIn(-1.0, 1.0) * Short.MAX_VALUE).toInt().toShort() buffer[i] = (sample.coerceIn(-1.0, 1.0) * Short.MAX_VALUE).toInt().toShort()
if (abs(sample) > 0.0001) {
allSamplesZero = false
}
} }
// Безопасная запись в AudioTrack // Безопасная запись в AudioTrack
@ -184,32 +182,32 @@ class AdvancedTubaSynth(private val context: Context) {
try { try {
val written = track.write(buffer, 0, buffer.size) val written = track.write(buffer, 0, buffer.size)
if (written < buffer.size) { if (written < buffer.size) {
// Проблема с записью - возможно, AudioTrack в плохом состоянии Thread.sleep(1)
Thread.sleep(1) // Небольшая пауза
} }
} catch (e: Exception) { } catch (e: Exception) {
// Игнорируем ошибки записи при остановке
if (!shouldStop) { if (!shouldStop) {
e.printStackTrace() e.printStackTrace()
// Пытаемся восстановить AudioTrack
android.os.Handler(android.os.Looper.getMainLooper()).post { android.os.Handler(android.os.Looper.getMainLooper()).post {
initializeAudioTrack() initializeAudioTrack()
} }
break continueGeneration = false
} }
} }
} else { } else {
// AudioTrack не готов - пытаемся восстановить
android.os.Handler(android.os.Looper.getMainLooper()).post { android.os.Handler(android.os.Looper.getMainLooper()).post {
initializeAudioTrack() initializeAudioTrack()
} }
break continueGeneration = false
} }
// Проверяем, нужно ли остановить генерацию // Останавливаем генерацию только если огибающая в состоянии idle
// и прошло достаточно времени для гарантии полного затухания
if (allSamplesZero && envelopeState == 0) {
synchronized(lock) { synchronized(lock) {
if (envelopeState == EnvelopeState.IDLE && !isNoteOn) { if (!isNoteOn) {
isPlaying = false isPlaying = false
continueGeneration = false
}
} }
} }
} }
@ -225,166 +223,102 @@ class AdvancedTubaSynth(private val context: Context) {
private fun generateSample(): Double { private fun generateSample(): Double {
synchronized(lock) { synchronized(lock) {
// Быстрое изменение частоты для легато
// Плавное изменение частоты для легато
if (abs(currentFrequency - targetFrequency) > 0.1) { if (abs(currentFrequency - targetFrequency) > 0.1) {
currentFrequency += (targetFrequency - currentFrequency) * 0.3f currentFrequency += (targetFrequency - currentFrequency) * 0.2f
} }
// Расчет огибающей // Обновление огибающей
val envelope = calculateEnvelope() updateEnvelope()
// Обновление счетчиков в зависимости от состояния // Если огибающая в состоянии idle, возвращаем 0
when (envelopeState) { if (envelopeState == 0) {
EnvelopeState.ATTACK, EnvelopeState.DECAY, EnvelopeState.SUSTAIN -> {
samplesSinceNoteOn++
}
EnvelopeState.RELEASE -> {
samplesSinceNoteOff++
}
EnvelopeState.IDLE -> {
// Ничего не делаем
}
}
// Проверяем переходы между состояниями
checkStateTransitions()
// Если звук полностью затух, возвращаем 0
if (envelopeState == EnvelopeState.IDLE) {
return 0.0 return 0.0
} }
// Генерация waveform с сохранением фазы для избежания щелчков // Генерация waveform с правильным тембром тубы
val wave = calculateTubaWaveform(currentFrequency) val wave = calculateTubaWaveform(currentFrequency)
return wave * envelope // В начале generateSample(), перед возвратом результата:
val result = wave * envelopeValue
// Очень мягкое ограничение на самых низких уровнях громкости
return if (abs(result) < 0.0001) {
result * (1.0 - exp(-abs(result) * 100.0))
} else {
result
}
// return wave * envelopeValue
} }
} }
private fun calculateEnvelope(): Double { private fun updateEnvelope() {
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) { when (envelopeState) {
EnvelopeState.ATTACK -> { 1 -> { // Атака
if (samplesSinceNoteOn >= attackTime) { envelopeCounter++
envelopeState = EnvelopeState.DECAY if (envelopeCounter >= attackSamples) {
envelopeValue = sustainLevel
envelopeState = 2 // сустейн
} else {
// Линейная атака
envelopeValue = (envelopeCounter.toDouble() / attackSamples) * sustainLevel
} }
} }
EnvelopeState.DECAY -> { 2 -> { // Суснейн
if (samplesSinceNoteOn >= attackTime + decayTime) { envelopeValue = sustainLevel
envelopeState = EnvelopeState.SUSTAIN // Если нота отпущена, переходим к релизу
}
}
EnvelopeState.SUSTAIN -> {
// SUSTAIN продолжается пока isNoteOn = true
if (!isNoteOn) { if (!isNoteOn) {
envelopeState = EnvelopeState.RELEASE envelopeState = 3
samplesSinceNoteOff = 0 envelopeCounter = 0
} }
} }
EnvelopeState.RELEASE -> { 3 -> { // Релиз
if (samplesSinceNoteOff >= releaseTime) { envelopeCounter++
envelopeState = EnvelopeState.IDLE if (envelopeCounter >= releaseSamples) {
envelopeValue = 0.0
envelopeState = 0 // idle
} else {
// Линейный релиз
envelopeValue = sustainLevel * (1.0 - envelopeCounter.toDouble() / releaseSamples)
} }
} }
EnvelopeState.IDLE -> {
// Остаемся в IDLE
}
} }
currentAmplitude = envelopeValue
} }
private fun calculateTubaWaveform(freq: Float): Double { private fun calculateTubaWaveform(freq: Float): Double {
// Используем фазовый накопитель для избежания щелчков при смене нот // Используем фазовый накопитель для избежания щелчков
val phaseIncrement = 2.0 * PI * freq / SAMPLE_RATE val phaseIncrement = 2.0 * PI * freq / SAMPLE_RATE
phase += phaseIncrement phase += phaseIncrement
// Сбрасываем фазу при переполнении для избежания потери точности // Сбрасываем фазу при переполнении
if (phase > 2.0 * PI) { if (phase > 2.0 * PI) {
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 fundamental = sin(phase)
val overtones = when (range) { // Оптимизированные обертоны для тембра тубы
"very_low" -> { val overtones =
sin(2.0 * phase) * 0.6 + sin(2.0 * phase) * 0.5 + // октава
sin(3.0 * phase) * 0.5 + sin(3.0 * phase) * 0.3 + // квинта
sin(4.0 * phase) * 0.4 sin(4.0 * phase) * 0.2 + // октава
} sin(5.0 * phase) * 0.15 + // большая терция
"low" -> { sin(6.0 * phase) * 0.1 // квинта
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 val mixed = fundamental * 0.7 + overtones * 0.6
// Мягкое ограничение для теплого звука
return tanh(mixed * 1.5) / 1.5
} }
fun isActive(): Boolean { fun isActive(): Boolean {
synchronized(lock) { synchronized(lock) {
return isNoteOn || envelopeState != EnvelopeState.IDLE return isNoteOn || envelopeState != 0
} }
} }
@ -401,7 +335,7 @@ class AdvancedTubaSynth(private val context: Context) {
// Безопасная остановка потока // Безопасная остановка потока
currentThread?.let { thread -> currentThread?.let { thread ->
try { try {
thread.join(100) // Ждем завершения потока до 100ms thread.join(100)
} catch (e: InterruptedException) { } catch (e: InterruptedException) {
thread.interrupt() thread.interrupt()
} }