BASSMIDI on android not playing first note of a midi file if its not tick 2000

Started by kittentaser, 25 Feb '25 - 23:02

kittentaser

Hi.

Ive been attempting to set up a midi file player on android. The goal was to have any tracks other than midi track 0 use a hardcoded preset call but have track 0 use a preset ive selected in a menu. As you can see i tried everything to get this thing to play the midi file with the preset ive selected,  Its definitely decoding to the right preset, but unless i offset all my note events by 2000 ticks, its not playing that first note.

Here is the code for playing the midi file i omitted portions responsible for actually finding the file and am including just those directly referencing bassmidi:
AsyncFunction("playMidiFile") { midiFilePath: String, bpm: Int ->
        ensureInitialized()
        stopSequenceStream()
   
           
        // Create stream (synchronous processing; no async flag)
        val fileFlags = BASS.BASS_SAMPLE_FLOAT or
                        BASS.BASS_SAMPLE_SOFTWARE or
                        BASS_SAMPLE_NOBUFFER or
                        BASSMIDI.BASS_MIDI_DECAYEND
   
        sequenceStream = BASSMIDI.BASS_MIDI_StreamCreateFile(finalPath, 0L, 0L, fileFlags, 44100)
        if (sequenceStream == 0) {
            val err = BASS.BASS_ErrorGetCode()
            Log.e(TAG, "BASS_MIDI_StreamCreateFile failed => err=$err path=$midiFilePath")
            throw Exception("BASS_MIDI_StreamCreateFile failed with error code $err for path $midiFilePath")
        }
        logStreamInfo(sequenceStream, "SequenceStream")
   
        // Get current preset configuration
        val (bank, preset) = getBankAndPreset(instrumentSound)
        currentBank = bank
        currentPreset = preset
   
        // ===== CRITICAL PRESET INITIALIZATION =====
        val fontConfig = BASSMIDI.BASS_MIDI_FONTEX2().apply {
            font = defaultFontHandle
            spreset = preset    // force source preset to the desired preset
            sbank = bank        // force source bank to the desired bank
            dpreset = preset    // destination preset
            dbank = bank        // destination bank
            dbanklsb = 0
            minchan = 0         // apply mapping starting at channel 0
            numchan = 1         // apply mapping to all channels (adjust if needed)
        }
        if (!BASSMIDI.BASS_MIDI_StreamSetFonts(sequenceStream, arrayOf(fontConfig), 1)) {
            Log.e(TAG, "Font setup failed: ${BASS.BASS_ErrorGetCode()}")
        }
   
        // Lock channel and send reset/control messages
        BASS.BASS_ChannelLock(sequenceStream, true)
        BASSMIDI.BASS_MIDI_StreamEvent(sequenceStream, 0, BASSMIDI.MIDI_EVENT_RESET, 0)
        BASSMIDI.BASS_MIDI_StreamEvent(sequenceStream, 0, BASSMIDI.MIDI_EVENT_BANK, bank)
        BASSMIDI.BASS_MIDI_StreamEvent(sequenceStream, 0, BASSMIDI.MIDI_EVENT_BANK_LSB, 0)
        BASSMIDI.BASS_MIDI_StreamEvent(sequenceStream, 0, BASSMIDI.MIDI_EVENT_PROGRAM, preset)
        BASSMIDI.BASS_MIDI_StreamEvent(sequenceStream, 0, BASSMIDI.MIDI_EVENT_VOLUME, 127)
        BASSMIDI.BASS_MIDI_StreamEvent(sequenceStream, 0, BASSMIDI.MIDI_EVENT_EXPRESSION, 127)
       
        // Flush any pre-buffered events
        BASS.BASS_ChannelUpdate(sequenceStream, Int.MAX_VALUE)
        BASS.BASS_ChannelSetPosition(sequenceStream, 0, BASSMIDI.BASS_POS_MIDI_TICK)
        BASS.BASS_ChannelLock(sequenceStream, false)
       
        val loadedPreset = BASSMIDI.BASS_MIDI_StreamGetEvent(sequenceStream, 0, BASSMIDI.MIDI_EVENT_PROGRAM)
        Log.d(TAG, "Current preset verification: ${loadedPreset and 0xFF} (expected $preset)")
   
        // --- Extract first note event dynamically ---
        var firstNoteChannel = -1
        var firstNoteMidi = -1
        var firstNoteVelocity = -1
        // Allocate an array for a few events (assuming the first note is among them)
        val eventsArray = arrayOfNulls<BASSMIDI.BASS_MIDI_EVENT>(10)
        val numEvents = BASSMIDI.BASS_MIDI_StreamGetEvents(sequenceStream, 0, 0, eventsArray as Array<BASSMIDI.BASS_MIDI_EVENT>)
        if (numEvents > 0) {
            for (i in 0 until numEvents) {
                val ev = eventsArray[i]
                if (ev != null && ev.event == BASSMIDI.MIDI_EVENT_NOTE) {
                    // Extract velocity (upper 8 bits) and note (lower 7 bits)
                    val velocity = ev.param shr 8
                    if (velocity > 0) { // note-on event
                        firstNoteChannel = ev.chan
                        firstNoteMidi = ev.param and 0x7F
                        firstNoteVelocity = velocity
                        break
                    }
                }
            }
        }
        // Fallback defaults if extraction failed
        if (firstNoteChannel == -1) {
            firstNoteChannel = 0
            firstNoteMidi = 60
            firstNoteVelocity = 76
        }
        Log.d(TAG, "First note detected: channel $firstNoteChannel, note $firstNoteMidi, velocity $firstNoteVelocity")
   
        // Stop the stream to flush any pending events, then reposition to tick 2
        BASS.BASS_ChannelStop(sequenceStream)
        BASS.BASS_ChannelSetPosition(sequenceStream, 2, BASSMIDI.BASS_POS_MIDI_TICK)
       
        // Configure tempo
        val usPerQuarter = 60_000_000L / bpm
        BASSMIDI.BASS_MIDI_StreamEvent(sequenceStream, 0, BASSMIDI.MIDI_EVENT_TEMPO, usPerQuarter.toInt())
       
        // Set volume and start playback with restart flag true
        BASS.BASS_ChannelSetAttribute(sequenceStream, com.un4seen.bass.BASSMIDI.BASS_ATTRIB_MIDI_VOL, 1.0f)
        BASS.BASS_ChannelSetAttribute(sequenceStream, BASS.BASS_ATTRIB_VOL, 1.0f)
        BASS.BASS_ChannelPlay(sequenceStream, true)
       
       
        // Reinforce preset shortly after starting playback
        Handler(Looper.getMainLooper()).postDelayed({
            BASSMIDI.BASS_MIDI_StreamEvent(sequenceStream, 0, BASSMIDI.MIDI_EVENT_PROGRAM, preset)
            BASSMIDI.BASS_MIDI_StreamEvent(sequenceStream, 0, BASSMIDI.MIDI_EVENT_VOLUME, 127)
            Log.d(TAG, "Post-start preset reinforcement")
        }, 50)
       
        sequenceStartTime = System.currentTimeMillis()
        startPositionLogger()
        isSequencePlaying = true
       
        val endSyncProc = object : SYNCPROC {
            override fun SYNCPROC(handle: Int, channel: Int, data: Int, user: Any?) {
                Handler(Looper.getMainLooper()).post {
                    val elapsed = System.currentTimeMillis() - sequenceStartTime
                    Log.e(TAG, "MIDI finished => took $elapsed ms")
                    sendEvent("SequenceFinishedTime", bundleOf("elapsedMs" to elapsed))
                    sendEvent(IS_PLAYING_DID_CHANGE, bundleOf("isSequencePlaying" to false))
                    isSequencePlaying = false
                    stopSequenceStream()
                }
            }
        }
        BASS.BASS_ChannelSetSync(sequenceStream, BASSMIDI.BASS_SYNC_MIDI_EVENT, 0, endSyncProc, null)
    }

As you can see i've tried all kinds of things. I am hoping you might have a much easier way forward to play my midi files starting at a realistic tick (0 would be nice, but i understand if thats not possible, happy to offset everything by a smaller tick offset than 2000) without losing that first note.

Ian @ un4seen

If I understand correctly, you basically want to override a MIDI file's MIDI_EVENT_PROGRAM events? If so, the best way to do that is with BASS_MIDI_StreamSetFilter and a MIDIFILTERPROC callback function. For example, the MIDIFILTERPROC could look something like this:

BOOL CALLBACK MidiFilterProc(HSTREAM handle, int track, BASS_MIDI_EVENT *event, BOOL seeking, void *user)
{
if (event->event == MIDI_EVENT_BANK || event->event == MIDI_EVENT_BANK_LSB) // got a bank change
return FALSE; // ignore it
if (event->event == MIDI_EVENT_PROGRAM) { // got a program/preset change
if (event->chan == 0)
event->value = program0; // set channel 0 value
else
event->value = programother; // set other channel value
}
return TRUE; // process the event
}

If you also (or instead) want to check the track number then you can use the "track" parameter for that. Please see the documentation for more info.

kittentaser

Hi Ian,

Thanks again for your guidance. I've integrated your suggestion to override a MIDI file's MIDI_EVENT_PROGRAM events using BASS_MIDI_StreamSetFilter and a MIDIFILTERPROC callback. Below are the relevant sections of my code. Ignore my debug logs they are just trying to help me understand the problem. (kotlin)

For the filter, I'm doing the following:

private val midiFilterProc = object : MIDIFILTERPROC {
    override fun MIDIFILTERPROC(
        handle: Int,
        track: Int,
        event: BASS_MIDI_EVENT?,
        seeking: Boolean,
        user: Any?
    ): Boolean {
        if (event == null) return true
        when (event.event) {
            BASSMIDI.MIDI_EVENT_BANK,
            BASSMIDI.MIDI_EVENT_BANK_LSB -> {
                return false
            }
            BASSMIDI.MIDI_EVENT_PROGRAM -> {
                if (event.chan == 0) {
                    event.param = currentPreset
                }
            }
            BASSMIDI.MIDI_EVENT_NOTE -> {
                val note = event.param and 0x7F
                val velocity = (event.param shr 8) and 0xFF
                if (velocity == 0) {
                    debugLog(DebugConfig.DEBUG_BASSMIDI, TAG, "Filter: Channel 0 Note OFF - note: $note")
                } else {
                    debugLog(DebugConfig.DEBUG_BASSMIDI, TAG, "Filter: Channel 0 Note ON - note: $note, velocity: $velocity")
                }
            }
        }
        return true
    }
}
 

And in my playMidiFile function I'm setting up the stream like this:

val fileFlags = BASS.BASS_SAMPLE_FLOAT or
                BASS.BASS_SAMPLE_SOFTWARE or
                BASSMIDI.BASS_MIDI_DECAYEND
sequenceStream = BASSMIDI.BASS_MIDI_StreamCreateFile(
    finalPath, 0L, 0L, fileFlags, 44100)
if (sequenceStream == 0) {
    throw Exception("BASS_MIDI_StreamCreateFile failed")
}
BASSMIDI.BASS_MIDI_StreamSetFilter(sequenceStream, true, midiFilterProc, null)
val patchParam = (bank shl 16) or (preset and 0xffff)
BASSMIDI.BASS_MIDI_StreamEvent(
    sequenceStream, 0, BASSMIDI.MIDI_EVENT_PROGRAM, patchParam)
BASSMIDI.BASS_MIDI_StreamEvent(
    sequenceStream, 0, BASSMIDI.MIDI_EVENT_MASTERVOL, 16383)
BASSMIDI.BASS_MIDI_StreamEvent(
    sequenceStream, 0, BASSMIDI.MIDI_EVENT_VOLUME, 127)
BASSMIDI.BASS_MIDI_StreamEvent(
    sequenceStream, 0, BASSMIDI.MIDI_EVENT_EXPRESSION, 127)
BASS.BASS_ChannelUpdate(sequenceStream, Int.MAX_VALUE)
BASS.BASS_ChannelSetPosition(
    sequenceStream, 0, BASSMIDI.BASS_POS_MIDI_TICK)
BASS.BASS_ChannelStop(sequenceStream)
BASS.BASS_ChannelSetPosition(
    sequenceStream, 2, BASSMIDI.BASS_POS_MIDI_TICK)
BASS.BASS_ChannelPlay(sequenceStream, false)

And my global initialization for BASS/BASSMIDI is:

BASS.BASS_Init(-1, 44100, 0)
defaultFontHandle = BASSMIDI.BASS_MIDI_FontInit(localSf2, 0)
BASSMIDI.BASS_MIDI_FontSetVolume(defaultFontHandle, 1.0f)

I have a realtime stream (using the same soundfont with the same preset) and it works fine, playing a full MIDI file produces no sound. The logs show that the filter is receiving events and overriding them (for example, "Filter: Overriding program on channel 0..."), and I see the volume, master volume, and expression events being sent. Yet the overall output level remains zero and no audio is heard during playback (though note off events are logged as expected).

It seems that—even with the filter correctly overriding the events—the stream isn't making sound with the midi file (which is about 3 seconds). The flush/reset steps I tried (setting the position to 0, stopping, and then repositioning to tick 2) don't seem to be enough.

Could you please advise if there's anything else I should adjust in how I'm flushing or initializing the stream? Or do you have any further insights on why the full MIDI file remains silent while my real time stream works?

Thanks in advance for your help.

Ian @ un4seen

If there's no sound, it could be because the MIDI stream doesn't have a soundfont set. Are you using BASS_MIDI_StreamSetFonts to do that, and is that call and the BASS_MIDI_FontInit call(s) reporting success in their return values? And if so, what does a BASS_MIDI_StreamGetFonts call on the stream return?

Please also try removing those BASS_ChannelUpdate and BASS_ChannelSetPosition calls, as they seem unnecessary. Or is there a reason for them?