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