diff --git a/app/src/main/java/net/sergeych/karabass/AdvancedTubaSynth.kt b/app/src/main/java/net/sergeych/karabass/AdvancedTubaSynth.kt index 02e854c..b4eacea 100644 --- a/app/src/main/java/net/sergeych/karabass/AdvancedTubaSynth.kt +++ b/app/src/main/java/net/sergeych/karabass/AdvancedTubaSynth.kt @@ -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() }