diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 96d23e1..a88dde5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -22,8 +22,8 @@ android { // (android.media.midi) and AAudio low-latency audio we rely on. minSdk = 26 targetSdk = 34 - versionCode = 12 - versionName = "0.4.5" + versionCode = 13 + versionName = "0.4.6" // We provide our own instrumentation runner if/when tests are added. testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/app/src/main/java/space/rcmd/android/sizzle/audio/AudioEngine.kt b/app/src/main/java/space/rcmd/android/sizzle/audio/AudioEngine.kt index 0169e47..2e69234 100644 --- a/app/src/main/java/space/rcmd/android/sizzle/audio/AudioEngine.kt +++ b/app/src/main/java/space/rcmd/android/sizzle/audio/AudioEngine.kt @@ -144,6 +144,16 @@ class AudioEngine( /** Scratch buffer: per-channel sum for one sample (audio-thread only). */ private val channelSum = FloatArray(Pattern.TRACK_COUNT) + // Per-block active-bus flags (audio-thread only). Rebuilt once per block so the + // per-sample mix loop can skip whole idle buses — and the entire bus-audition pool + // when nothing is being auditioned — instead of calling render() on all + // POLYPHONY*TRACK_COUNT pooled voices every sample (the fixed cost that dominated + // the render budget even at idle). The sequencer sets [blockBusActive] the instant + // it triggers a note mid-block, so a newly-started note still sounds sample- + // accurately within the same block. + private val blockBusActive = BooleanArray(Pattern.TRACK_COUNT) + private val blockBusAudition = BooleanArray(Pattern.TRACK_COUNT) + /** The song being played. Swapped atomically; read on the audio thread. */ @Volatile private var project: Project? = null @@ -519,6 +529,25 @@ class AudioEngine( // mid-block; indexing + array iteration below then allocate nothing. val chains = channelChains + // Once per block: flag which buses have any active voices, so the per-sample + // loop only renders those (see [blockBusActive]). Scanning the pools once here + // costs O(pool); the alternative — calling render() on every pooled voice every + // sample — costs O(pool * frames) and was the bulk of the idle render budget. + for (bus in 0 until Pattern.TRACK_COUNT) { + val base = bus * POLYPHONY + var seqAct = false + for (i in 0 until POLYPHONY) { + if (seqVoices[base + i].isActive || sampleVoices[base + i].isActive) { seqAct = true; break } + } + blockBusActive[bus] = seqAct + val bbase = bus * BUS_POLYPHONY + var busAct = false + for (i in 0 until BUS_POLYPHONY) { + if (busVoices[bbase + i].isActive || busSampleVoices[bbase + i].isActive) { busAct = true; break } + } + blockBusAudition[bus] = busAct + } + for (i in 0 until frames) { if (playing && proj != null) advanceSequencer(proj) @@ -526,14 +555,23 @@ class AudioEngine( var mixR = 0f if (proj != null) { channelSum.fill(0f) - // Pools are laid out bus*POLYPHONY + i, so the bus is vi / POLYPHONY. - for (vi in seqVoices.indices) { - channelSum[vi / POLYPHONY] += seqVoices[vi].render() + sampleVoices[vi].render() - } - // Live bus-audition voices feed the same per-channel sum, so they - // run through the channel's FX/volume/mute just like the sequencer. - for (bi in busVoices.indices) { - channelSum[bi / BUS_POLYPHONY] += busVoices[bi].render() + busSampleVoices[bi].render() + // Sum each bus's pooled voices, skipping buses whose pools are idle. + // Pools are laid out bus*POLYPHONY + i / bus*BUS_POLYPHONY + i. + for (bus in 0 until Pattern.TRACK_COUNT) { + if (blockBusActive[bus]) { + val base = bus * POLYPHONY + var sum = channelSum[bus] + for (v in 0 until POLYPHONY) sum += seqVoices[base + v].render() + sampleVoices[base + v].render() + channelSum[bus] = sum + } + // Live bus-audition voices feed the same per-channel sum, so they + // run through the channel's FX/volume/mute just like the sequencer. + if (blockBusAudition[bus]) { + val bbase = bus * BUS_POLYPHONY + var sum = channelSum[bus] + for (v in 0 until BUS_POLYPHONY) sum += busVoices[bbase + v].render() + busSampleVoices[bbase + v].render() + channelSum[bus] = sum + } } for (ch in 0 until Pattern.TRACK_COUNT) { val mc = proj.mixer.channels[ch] @@ -765,6 +803,10 @@ class AudioEngine( * by the sequencer and the arpeggiator. */ private fun playNote(proj: Project, bus: Int, note: Int, velocity: Int) { + // A note is starting on this bus mid-block; mark the bus active so the rest of + // this block's per-sample mix renders it (the once-per-block scan ran before + // this trigger). Without this the note would be silent until the next block. + blockBusActive[bus] = true val slot = proj.toolbox.getOrNull(proj.mixer.channels[bus].instrumentSlot) val vs = voiceSample(slot, note) when {