diff --git a/app/src/main/java/net/sergeych/karabass/AdvancedTubaSynth.kt b/app/src/main/java/net/sergeych/karabass/AdvancedTubaSynth.kt index 1709646..02e854c 100644 --- a/app/src/main/java/net/sergeych/karabass/AdvancedTubaSynth.kt +++ b/app/src/main/java/net/sergeych/karabass/AdvancedTubaSynth.kt @@ -1,5 +1,4 @@ package net.sergeych.karabass - import android.content.Context import android.media.AudioAttributes import android.media.AudioFormat @@ -7,6 +6,7 @@ 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) { @@ -63,6 +63,57 @@ class AdvancedTubaSynth(private val context: Context) { private val releaseTime = (0.05 * SAMPLE_RATE).toInt() // 50ms релиз private val sustainLevel = 0.8 + // Минимальный буфер для low-latency + private val bufferSize = 512 // ~11ms latency + + init { + // Создаем AudioTrack один раз при инициализации + 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() + // Пытаемся восстановить AudioTrack через некоторое время + android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({ + initializeAudioTrack() + }, 100) + } + } + } + fun startNote(frequency: Float) { synchronized(lock) { targetFrequency = frequency @@ -105,47 +156,15 @@ 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 + // Проверяем, что AudioTrack существует и готов + val track = audioTrack + if (track == null || track.state != AudioTrack.STATE_INITIALIZED) { + initializeAudioTrack() } currentThread = Thread { @@ -161,17 +180,29 @@ class AdvancedTubaSynth(private val context: Context) { // Безопасная запись в 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) { + // Проблема с записью - возможно, AudioTrack в плохом состоянии + Thread.sleep(1) // Небольшая пауза + } } catch (e: Exception) { // Игнорируем ошибки записи при остановке if (!shouldStop) { e.printStackTrace() + // Пытаемся восстановить AudioTrack + android.os.Handler(android.os.Looper.getMainLooper()).post { + initializeAudioTrack() + } + break } - break } } else { + // AudioTrack не готов - пытаемся восстановить + android.os.Handler(android.os.Looper.getMainLooper()).post { + initializeAudioTrack() + } break } @@ -185,12 +216,6 @@ class AdvancedTubaSynth(private val context: Context) { } catch (e: Exception) { e.printStackTrace() } finally { - // Безопасная остановка - try { - audioTrack?.stop() - } catch (e: Exception) { - e.printStackTrace() - } isPlaying = false } }.apply { diff --git a/app/src/main/java/net/sergeych/karabass/RealisticTubaSynth.kt b/app/src/main/java/net/sergeych/karabass/RealisticTubaSynth.kt deleted file mode 100644 index 0ce706f..0000000 --- a/app/src/main/java/net/sergeych/karabass/RealisticTubaSynth.kt +++ /dev/null @@ -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() - } -} \ No newline at end of file diff --git a/app/src/main/java/net/sergeych/karabass/TubaSynth.kt b/app/src/main/java/net/sergeych/karabass/TubaSynth.kt deleted file mode 100644 index 6247366..0000000 --- a/app/src/main/java/net/sergeych/karabass/TubaSynth.kt +++ /dev/null @@ -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 - } -} \ No newline at end of file