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