Compare commits

...

2 Commits

Author SHA1 Message Date
c87585693f less clicks on note end/start 2025-10-13 12:24:46 +04:00
fc12c9d932 fixed bug with audio track creation failure 2025-10-13 11:27:18 +04:00
3 changed files with 156 additions and 656 deletions

View File

@ -12,7 +12,6 @@ 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,42 +43,89 @@ 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
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) {
synchronized(lock) {
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
}
}
@ -89,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
}
}
}
@ -105,92 +150,70 @@ class AdvancedTubaSynth(private val context: Context) {
}
private fun startAudioGeneration() {
if (isPlaying) return
shouldStop = false
isPlaying = true
// Минимальный буфер для low-latency
val bufferSize = 512 // ~11ms latency
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
val track = audioTrack
if (track == null || track.state != AudioTrack.STATE_INITIALIZED) {
initializeAudioTrack()
}
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
val track = audioTrack
if (track?.playState == AudioTrack.PLAYSTATE_PLAYING) {
if (track?.state == AudioTrack.STATE_INITIALIZED && track.playState == AudioTrack.PLAYSTATE_PLAYING) {
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) {
// Игнорируем ошибки записи при остановке
if (!shouldStop) {
e.printStackTrace()
android.os.Handler(android.os.Looper.getMainLooper()).post {
initializeAudioTrack()
}
continueGeneration = false
}
break
}
} else {
break
android.os.Handler(android.os.Looper.getMainLooper()).post {
initializeAudioTrack()
}
continueGeneration = false
}
// Проверяем, нужно ли остановить генерацию
synchronized(lock) {
if (envelopeState == EnvelopeState.IDLE && !isNoteOn) {
isPlaying = false
// Останавливаем генерацию только если огибающая в состоянии idle
// и прошло достаточно времени для гарантии полного затухания
if (allSamplesZero && envelopeState == 0) {
synchronized(lock) {
if (!isNoteOn) {
isPlaying = false
continueGeneration = false
}
}
}
}
} catch (e: Exception) {
e.printStackTrace()
} finally {
// Безопасная остановка
try {
audioTrack?.stop()
} catch (e: Exception) {
e.printStackTrace()
}
isPlaying = false
}
}.apply {
@ -200,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
}
}
@ -376,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()
}

View File

@ -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()
}
}

View File

@ -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
}
}