Compare commits
2 Commits
e068ad52de
...
c87585693f
Author | SHA1 | Date | |
---|---|---|---|
c87585693f | |||
fc12c9d932 |
@ -12,7 +12,6 @@ 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,42 +43,89 @@ 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
|
||||||
|
private val bufferSize = 512
|
||||||
|
|
||||||
|
init {
|
||||||
|
initializeAudioTrack()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initializeAudioTrack() {
|
||||||
|
synchronized(lock) {
|
||||||
|
try {
|
||||||
|
audioTrack = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
val attributes = AudioAttributes.Builder()
|
||||||
|
.setUsage(AudioAttributes.USAGE_MEDIA)
|
||||||
|
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val format = AudioFormat.Builder()
|
||||||
|
.setSampleRate(SAMPLE_RATE)
|
||||||
|
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
|
||||||
|
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
AudioTrack.Builder()
|
||||||
|
.setAudioAttributes(attributes)
|
||||||
|
.setAudioFormat(format)
|
||||||
|
.setBufferSizeInBytes(bufferSize * 2)
|
||||||
|
.build()
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
AudioTrack(
|
||||||
|
AudioManager.STREAM_MUSIC,
|
||||||
|
SAMPLE_RATE,
|
||||||
|
AudioFormat.CHANNEL_OUT_MONO,
|
||||||
|
AudioFormat.ENCODING_PCM_16BIT,
|
||||||
|
bufferSize * 2,
|
||||||
|
AudioTrack.MODE_STREAM
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
audioTrack?.play()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
|
||||||
|
initializeAudioTrack()
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun startNote(frequency: Float) {
|
fun startNote(frequency: Float) {
|
||||||
synchronized(lock) {
|
synchronized(lock) {
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -89,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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -105,92 +150,70 @@ class AdvancedTubaSynth(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun startAudioGeneration() {
|
private fun startAudioGeneration() {
|
||||||
|
if (isPlaying) return
|
||||||
|
|
||||||
shouldStop = false
|
shouldStop = false
|
||||||
isPlaying = true
|
isPlaying = true
|
||||||
|
|
||||||
// Минимальный буфер для low-latency
|
val track = audioTrack
|
||||||
val bufferSize = 512 // ~11ms latency
|
if (track == null || track.state != AudioTrack.STATE_INITIALIZED) {
|
||||||
|
initializeAudioTrack()
|
||||||
try {
|
|
||||||
audioTrack = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
val attributes = AudioAttributes.Builder()
|
|
||||||
.setUsage(AudioAttributes.USAGE_MEDIA)
|
|
||||||
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val format = AudioFormat.Builder()
|
|
||||||
.setSampleRate(SAMPLE_RATE)
|
|
||||||
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
|
|
||||||
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
AudioTrack.Builder()
|
|
||||||
.setAudioAttributes(attributes)
|
|
||||||
.setAudioFormat(format)
|
|
||||||
.setBufferSizeInBytes(bufferSize * 2)
|
|
||||||
.build()
|
|
||||||
} else {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
AudioTrack(
|
|
||||||
AudioManager.STREAM_MUSIC,
|
|
||||||
SAMPLE_RATE,
|
|
||||||
AudioFormat.CHANNEL_OUT_MONO,
|
|
||||||
AudioFormat.ENCODING_PCM_16BIT,
|
|
||||||
bufferSize * 2,
|
|
||||||
AudioTrack.MODE_STREAM
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
audioTrack?.play()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
isPlaying = false
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
val track = audioTrack
|
val track = audioTrack
|
||||||
if (track?.playState == AudioTrack.PLAYSTATE_PLAYING) {
|
if (track?.state == AudioTrack.STATE_INITIALIZED && track.playState == AudioTrack.PLAYSTATE_PLAYING) {
|
||||||
try {
|
try {
|
||||||
track.write(buffer, 0, buffer.size)
|
val written = track.write(buffer, 0, buffer.size)
|
||||||
|
if (written < buffer.size) {
|
||||||
|
Thread.sleep(1)
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Игнорируем ошибки записи при остановке
|
|
||||||
if (!shouldStop) {
|
if (!shouldStop) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
|
android.os.Handler(android.os.Looper.getMainLooper()).post {
|
||||||
|
initializeAudioTrack()
|
||||||
|
}
|
||||||
|
continueGeneration = false
|
||||||
}
|
}
|
||||||
break
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
break
|
android.os.Handler(android.os.Looper.getMainLooper()).post {
|
||||||
|
initializeAudioTrack()
|
||||||
|
}
|
||||||
|
continueGeneration = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем, нужно ли остановить генерацию
|
// Останавливаем генерацию только если огибающая в состоянии idle
|
||||||
synchronized(lock) {
|
// и прошло достаточно времени для гарантии полного затухания
|
||||||
if (envelopeState == EnvelopeState.IDLE && !isNoteOn) {
|
if (allSamplesZero && envelopeState == 0) {
|
||||||
isPlaying = false
|
synchronized(lock) {
|
||||||
|
if (!isNoteOn) {
|
||||||
|
isPlaying = false
|
||||||
|
continueGeneration = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
} finally {
|
} finally {
|
||||||
// Безопасная остановка
|
|
||||||
try {
|
|
||||||
audioTrack?.stop()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
isPlaying = false
|
isPlaying = false
|
||||||
}
|
}
|
||||||
}.apply {
|
}.apply {
|
||||||
@ -200,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -376,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()
|
||||||
}
|
}
|
||||||
|
@ -1,260 +0,0 @@
|
|||||||
import android.content.Context
|
|
||||||
import android.media.AudioAttributes
|
|
||||||
import android.media.AudioFormat
|
|
||||||
import android.media.AudioManager
|
|
||||||
import android.media.AudioTrack
|
|
||||||
import android.os.Build
|
|
||||||
import kotlin.math.*
|
|
||||||
import kotlin.random.Random
|
|
||||||
|
|
||||||
class RealisticTubaSynth(private val context: Context) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val SAMPLE_RATE = 44100
|
|
||||||
const val CUTOFF_FREQ = 800.0
|
|
||||||
}
|
|
||||||
|
|
||||||
private var audioTrack: AudioTrack? = null
|
|
||||||
private var isPlaying = false
|
|
||||||
private var currentThread: Thread? = null
|
|
||||||
|
|
||||||
data class TubaNote(
|
|
||||||
val frequency: Float,
|
|
||||||
val amplitude: Float = 0.8f,
|
|
||||||
val durationMs: Int
|
|
||||||
)
|
|
||||||
|
|
||||||
fun playTubaNote(frequency: Float, durationMs: Int = 1500) {
|
|
||||||
// Останавливаем предыдущее воспроизведение
|
|
||||||
stop()
|
|
||||||
|
|
||||||
val note = TubaNote(frequency, 0.8f, durationMs)
|
|
||||||
generateTubaSound(note)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun generateTubaSound(note: TubaNote) {
|
|
||||||
currentThread = Thread {
|
|
||||||
isPlaying = true
|
|
||||||
val numSamples = note.durationMs * SAMPLE_RATE / 1000
|
|
||||||
val buffer = ShortArray(numSamples)
|
|
||||||
|
|
||||||
// Фильтр для шума дыхания
|
|
||||||
val alpha = 1 - exp(-2.0 * PI * CUTOFF_FREQ / SAMPLE_RATE)
|
|
||||||
var filteredNoise = 0.0
|
|
||||||
|
|
||||||
val random = Random(System.currentTimeMillis())
|
|
||||||
|
|
||||||
for (i in 0 until numSamples) {
|
|
||||||
if (!isPlaying) break
|
|
||||||
|
|
||||||
val time = i.toDouble() / SAMPLE_RATE
|
|
||||||
|
|
||||||
// Основной waveform тубы с более богатым спектром
|
|
||||||
val tubaWave = calculateTubaWaveform(note.frequency, time)
|
|
||||||
|
|
||||||
// Шум дыхания с фильтром низких частот
|
|
||||||
val whiteNoise = random.nextDouble() * 2 - 1
|
|
||||||
filteredNoise += alpha * (whiteNoise - filteredNoise)
|
|
||||||
|
|
||||||
// Огибающая с резкой атакой и быстрым спадом
|
|
||||||
val envelope = getRealisticTubaEnvelope(i, numSamples, note.frequency)
|
|
||||||
|
|
||||||
// Усиление басов
|
|
||||||
val bassBoost = 1.0 + 0.5 * sin(2 * PI * 60.0 * time)
|
|
||||||
|
|
||||||
// Смешиваем всё вместе
|
|
||||||
var sample = tubaWave + filteredNoise * 0.03 * envelope
|
|
||||||
sample *= envelope * bassBoost
|
|
||||||
|
|
||||||
// Ограничиваем амплитуду
|
|
||||||
sample = sample.coerceIn(-1.0, 1.0)
|
|
||||||
|
|
||||||
buffer[i] = (sample * Short.MAX_VALUE).toInt().toShort()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPlaying) {
|
|
||||||
playAudioBuffer(buffer)
|
|
||||||
}
|
|
||||||
}.apply {
|
|
||||||
start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun calculateTubaWaveform(freq: Float, time: Double): Double {
|
|
||||||
// Классификация нот по диапазонам
|
|
||||||
val range = when {
|
|
||||||
freq < 40 -> "pedal" // Педальные ноты (очень низкие)
|
|
||||||
freq < 80 -> "low" // Низкий диапазон
|
|
||||||
freq < 160 -> "middle" // Средний диапазон
|
|
||||||
freq < 240 -> "high" // Высокий диапазон
|
|
||||||
else -> "very_high" // Очень высокий диапазон
|
|
||||||
}
|
|
||||||
|
|
||||||
val fundamental = sin(2 * PI * freq * time)
|
|
||||||
|
|
||||||
// Разные наборы обертонов для разных диапазонов
|
|
||||||
val overtones = when (range) {
|
|
||||||
"pedal" -> {
|
|
||||||
// Педальные ноты: сильные низкие обертоны, меньше высоких
|
|
||||||
sin(2 * PI * freq * 2 * time) * 0.6 +
|
|
||||||
sin(2 * PI * freq * 3 * time) * 0.5 +
|
|
||||||
sin(2 * PI * freq * 4 * time) * 0.4 +
|
|
||||||
sin(2 * PI * freq * 5 * time) * 0.3
|
|
||||||
}
|
|
||||||
"low" -> {
|
|
||||||
// Низкий диапазон: богатый спектр
|
|
||||||
sin(2 * PI * freq * 2 * time) * 0.5 +
|
|
||||||
sin(2 * PI * freq * 3 * time) * 0.45 +
|
|
||||||
sin(2 * PI * freq * 4 * time) * 0.4 +
|
|
||||||
sin(2 * PI * freq * 5 * time) * 0.35 +
|
|
||||||
sin(2 * PI * freq * 6 * time) * 0.25
|
|
||||||
}
|
|
||||||
"middle" -> {
|
|
||||||
// Средний диапазон: сбалансированный спектр
|
|
||||||
sin(2 * PI * freq * 2 * time) * 0.4 +
|
|
||||||
sin(2 * PI * freq * 3 * time) * 0.4 +
|
|
||||||
sin(2 * PI * freq * 4 * time) * 0.35 +
|
|
||||||
sin(2 * PI * freq * 5 * time) * 0.3 +
|
|
||||||
sin(2 * PI * freq * 6 * time) * 0.25 +
|
|
||||||
sin(2 * PI * freq * 7 * time) * 0.2
|
|
||||||
}
|
|
||||||
"high" -> {
|
|
||||||
// Высокий диапазон: больше высоких обертонов
|
|
||||||
sin(2 * PI * freq * 2 * time) * 0.3 +
|
|
||||||
sin(2 * PI * freq * 3 * time) * 0.35 +
|
|
||||||
sin(2 * PI * freq * 4 * time) * 0.3 +
|
|
||||||
sin(2 * PI * freq * 5 * time) * 0.25 +
|
|
||||||
sin(2 * PI * freq * 6 * time) * 0.2 +
|
|
||||||
sin(2 * PI * freq * 7 * time) * 0.15 +
|
|
||||||
sin(2 * PI * freq * 8 * time) * 0.1
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
// Очень высокий диапазон: яркий, с преобладанием высоких обертонов
|
|
||||||
sin(2 * PI * freq * 2 * time) * 0.25 +
|
|
||||||
sin(2 * PI * freq * 3 * time) * 0.3 +
|
|
||||||
sin(2 * PI * freq * 4 * time) * 0.25 +
|
|
||||||
sin(2 * PI * freq * 5 * time) * 0.2 +
|
|
||||||
sin(2 * PI * freq * 6 * time) * 0.15 +
|
|
||||||
sin(2 * PI * freq * 7 * time) * 0.1 +
|
|
||||||
sin(2 * PI * freq * 8 * time) * 0.08 +
|
|
||||||
sin(2 * PI * freq * 9 * time) * 0.05
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val mixed = fundamental * 0.6 + overtones * 0.8
|
|
||||||
return tanh(mixed * 1.2) / 1.2
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getRealisticTubaEnvelope(sampleIndex: Int, totalSamples: Int, frequency: Float): Double {
|
|
||||||
val position = sampleIndex.toDouble() / totalSamples
|
|
||||||
|
|
||||||
// Разная огибающая для разных диапазонов
|
|
||||||
val (attack, decay, sustain, release) = when {
|
|
||||||
frequency < 40 -> arrayOf(0.04, 0.2, 0.6, 0.15) // Педальные ноты: медленнее
|
|
||||||
frequency < 100 -> arrayOf(0.025, 0.15, 0.65, 0.1) // Низкие: умеренные
|
|
||||||
frequency < 200 -> arrayOf(0.015, 0.1, 0.7, 0.08) // Средние: быстрее
|
|
||||||
else -> arrayOf(0.01, 0.08, 0.75, 0.06) // Высокие: очень быстрые
|
|
||||||
}
|
|
||||||
|
|
||||||
return when {
|
|
||||||
position < attack -> (position / attack).pow(0.3)
|
|
||||||
position < attack + decay -> {
|
|
||||||
val decayPos = (position - attack) / decay
|
|
||||||
1.0 - (1.0 - sustain) * decayPos
|
|
||||||
}
|
|
||||||
position > 1 - release -> {
|
|
||||||
val releasePos = (position - (1 - release)) / release
|
|
||||||
sustain * (1 - releasePos)
|
|
||||||
}
|
|
||||||
else -> sustain
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private fun playAudioBuffer(buffer: ShortArray) {
|
|
||||||
try {
|
|
||||||
// Создаем новый AudioTrack каждый раз
|
|
||||||
val bufferSize = AudioTrack.getMinBufferSize(
|
|
||||||
SAMPLE_RATE,
|
|
||||||
AudioFormat.CHANNEL_OUT_MONO,
|
|
||||||
AudioFormat.ENCODING_PCM_16BIT
|
|
||||||
)
|
|
||||||
|
|
||||||
val newAudioTrack = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
val attributes = AudioAttributes.Builder()
|
|
||||||
.setUsage(AudioAttributes.USAGE_MEDIA)
|
|
||||||
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val format = AudioFormat.Builder()
|
|
||||||
.setSampleRate(SAMPLE_RATE)
|
|
||||||
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
|
|
||||||
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
AudioTrack.Builder()
|
|
||||||
.setAudioAttributes(attributes)
|
|
||||||
.setAudioFormat(format)
|
|
||||||
.setBufferSizeInBytes(bufferSize)
|
|
||||||
.build()
|
|
||||||
} else {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
AudioTrack(
|
|
||||||
AudioManager.STREAM_MUSIC,
|
|
||||||
SAMPLE_RATE,
|
|
||||||
AudioFormat.CHANNEL_OUT_MONO,
|
|
||||||
AudioFormat.ENCODING_PCM_16BIT,
|
|
||||||
bufferSize,
|
|
||||||
AudioTrack.MODE_STATIC
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
newAudioTrack.apply {
|
|
||||||
setVolume(0.8f)
|
|
||||||
|
|
||||||
// Используем MODE_STATIC для однократного воспроизведения
|
|
||||||
write(buffer, 0, buffer.size)
|
|
||||||
|
|
||||||
setPlaybackPositionUpdateListener(object : AudioTrack.OnPlaybackPositionUpdateListener {
|
|
||||||
override fun onMarkerReached(track: AudioTrack) {
|
|
||||||
// Автоматически освобождаем после воспроизведения
|
|
||||||
track.stop()
|
|
||||||
track.release()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPeriodicNotification(track: AudioTrack) {}
|
|
||||||
})
|
|
||||||
|
|
||||||
setNotificationMarkerPosition(buffer.size)
|
|
||||||
play()
|
|
||||||
}
|
|
||||||
|
|
||||||
audioTrack = newAudioTrack
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
isPlaying = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stop() {
|
|
||||||
isPlaying = false
|
|
||||||
currentThread?.interrupt()
|
|
||||||
currentThread = null
|
|
||||||
|
|
||||||
audioTrack?.let { track ->
|
|
||||||
try {
|
|
||||||
if (track.playState == AudioTrack.PLAYSTATE_PLAYING) {
|
|
||||||
track.stop()
|
|
||||||
}
|
|
||||||
track.release()
|
|
||||||
} catch (e: IllegalStateException) {
|
|
||||||
// Игнорируем ошибки при остановке уже остановленного трека
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
audioTrack = null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun release() {
|
|
||||||
stop()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,199 +0,0 @@
|
|||||||
package net.sergeych.karabass
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.media.AudioAttributes
|
|
||||||
import android.media.AudioFormat
|
|
||||||
import android.media.AudioManager
|
|
||||||
import android.media.AudioTrack
|
|
||||||
import android.os.Build
|
|
||||||
import kotlin.math.*
|
|
||||||
import kotlin.random.Random
|
|
||||||
|
|
||||||
class TubaSynth(private val context: Context) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val SAMPLE_RATE = 44100
|
|
||||||
const val CUTOFF_FREQ = 500.0
|
|
||||||
}
|
|
||||||
|
|
||||||
private var audioTrack: AudioTrack? = null
|
|
||||||
private var isPlaying = false
|
|
||||||
|
|
||||||
data class TubaNote(
|
|
||||||
val frequency: Float,
|
|
||||||
val amplitude: Float = 0.8f,
|
|
||||||
val durationMs: Int
|
|
||||||
)
|
|
||||||
|
|
||||||
fun playTubaNote(frequency: Float, durationMs: Int = 2000) {
|
|
||||||
if (isPlaying) {
|
|
||||||
stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
val note = TubaNote(frequency, 0.8f, durationMs)
|
|
||||||
generateTubaSound(note)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun generateTubaSound(note: TubaNote) {
|
|
||||||
Thread {
|
|
||||||
isPlaying = true
|
|
||||||
val numSamples = note.durationMs * SAMPLE_RATE / 1000
|
|
||||||
val buffer = ShortArray(numSamples)
|
|
||||||
|
|
||||||
// Фильтр для шума дыхания
|
|
||||||
val alpha = 1 - exp(-2.0 * PI * CUTOFF_FREQ / SAMPLE_RATE)
|
|
||||||
var filteredNoise = 0.0
|
|
||||||
|
|
||||||
val random = Random(System.currentTimeMillis())
|
|
||||||
|
|
||||||
for (i in 0 until numSamples) {
|
|
||||||
if (!isPlaying) break
|
|
||||||
|
|
||||||
val time = i.toDouble() / SAMPLE_RATE
|
|
||||||
|
|
||||||
// Основной waveform тубы
|
|
||||||
val tubaWave = calculateTubaWaveform(note.frequency, time)
|
|
||||||
|
|
||||||
// Шум дыхания с фильтром низких частот
|
|
||||||
val whiteNoise = random.nextDouble() * 2 - 1
|
|
||||||
filteredNoise += alpha * (whiteNoise - filteredNoise)
|
|
||||||
|
|
||||||
// Огибающая
|
|
||||||
val envelope = getTubaEnvelope(i, numSamples)
|
|
||||||
|
|
||||||
// Усиление басов
|
|
||||||
val bassBoost = 1.0 + 0.4 * sin(2 * PI * 80.0 * time)
|
|
||||||
|
|
||||||
// Смешиваем всё вместе
|
|
||||||
var sample = tubaWave + filteredNoise * 0.05 * envelope
|
|
||||||
sample *= envelope * bassBoost
|
|
||||||
|
|
||||||
// Ограничиваем амплитуду
|
|
||||||
sample = sample.coerceIn(-1.0, 1.0)
|
|
||||||
|
|
||||||
buffer[i] = (sample * Short.MAX_VALUE).toInt().toShort()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPlaying) {
|
|
||||||
playAudioBuffer(buffer)
|
|
||||||
}
|
|
||||||
}.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun calculateTubaWaveform(freq: Float, time: Double): Double {
|
|
||||||
return (
|
|
||||||
sin(2 * PI * freq * time) * 0.6 +
|
|
||||||
sin(2 * PI * freq * 2 * time) * 0.4 +
|
|
||||||
sin(2 * PI * freq * 3 * time) * 0.3 +
|
|
||||||
sin(2 * PI * freq * 4 * time) * 0.2 +
|
|
||||||
sin(2 * PI * freq * 5 * time) * 0.15 +
|
|
||||||
sin(2 * PI * freq * 6 * time) * 0.1 +
|
|
||||||
sin(2 * PI * freq * 7 * time) * 0.05
|
|
||||||
) * 0.6
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getTubaEnvelope(sampleIndex: Int, totalSamples: Int): Double {
|
|
||||||
val position = sampleIndex.toDouble() / totalSamples
|
|
||||||
|
|
||||||
val attack = 0.15
|
|
||||||
val decay = 0.1
|
|
||||||
val release = 0.3
|
|
||||||
|
|
||||||
return when {
|
|
||||||
position < attack -> {
|
|
||||||
val x = position / attack
|
|
||||||
x * x * (3 - 2 * x)
|
|
||||||
}
|
|
||||||
position < attack + decay -> {
|
|
||||||
val decayPos = (position - attack) / decay
|
|
||||||
0.9 + 0.1 * (1 - decayPos)
|
|
||||||
}
|
|
||||||
position > 1 - release -> {
|
|
||||||
val releasePos = (position - (1 - release)) / release
|
|
||||||
(1 - releasePos) * 0.9
|
|
||||||
}
|
|
||||||
else -> 0.9
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun playAudioBuffer(buffer: ShortArray) {
|
|
||||||
try {
|
|
||||||
// Останавливаем предыдущее воспроизведение
|
|
||||||
audioTrack?.stop()
|
|
||||||
audioTrack?.release()
|
|
||||||
|
|
||||||
val bufferSize = AudioTrack.getMinBufferSize(
|
|
||||||
SAMPLE_RATE,
|
|
||||||
AudioFormat.CHANNEL_OUT_MONO,
|
|
||||||
AudioFormat.ENCODING_PCM_16BIT
|
|
||||||
)
|
|
||||||
|
|
||||||
// Создаем AudioTrack с правильными параметрами
|
|
||||||
audioTrack = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
val attributes = AudioAttributes.Builder()
|
|
||||||
.setUsage(AudioAttributes.USAGE_MEDIA)
|
|
||||||
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val format = AudioFormat.Builder()
|
|
||||||
.setSampleRate(SAMPLE_RATE)
|
|
||||||
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
|
|
||||||
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
AudioTrack.Builder()
|
|
||||||
.setAudioAttributes(attributes)
|
|
||||||
.setAudioFormat(format)
|
|
||||||
.setBufferSizeInBytes(bufferSize)
|
|
||||||
.build()
|
|
||||||
} else {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
AudioTrack(
|
|
||||||
AudioManager.STREAM_MUSIC,
|
|
||||||
SAMPLE_RATE,
|
|
||||||
AudioFormat.CHANNEL_OUT_MONO,
|
|
||||||
AudioFormat.ENCODING_PCM_16BIT,
|
|
||||||
bufferSize,
|
|
||||||
AudioTrack.MODE_STREAM
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
audioTrack?.apply {
|
|
||||||
// Устанавливаем громкость
|
|
||||||
setVolume(0.8f)
|
|
||||||
|
|
||||||
// Воспроизводим
|
|
||||||
play()
|
|
||||||
|
|
||||||
// Пишем данные
|
|
||||||
write(buffer, 0, buffer.size)
|
|
||||||
|
|
||||||
// Ждем окончания воспроизведения
|
|
||||||
setNotificationMarkerPosition(buffer.size)
|
|
||||||
setPlaybackPositionUpdateListener(object : AudioTrack.OnPlaybackPositionUpdateListener {
|
|
||||||
override fun onMarkerReached(track: AudioTrack) {
|
|
||||||
stop()
|
|
||||||
release()
|
|
||||||
isPlaying = false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPeriodicNotification(track: AudioTrack) {}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
isPlaying = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stop() {
|
|
||||||
isPlaying = false
|
|
||||||
audioTrack?.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun release() {
|
|
||||||
stop()
|
|
||||||
audioTrack?.release()
|
|
||||||
audioTrack = null
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user