Page 1 of 1

Halt Fire please!! (Midi Sound) [Windows]

Posted: Wed Dec 27, 2023 5:24 pm
by minimy
Hello PBmakers!, this is a simple demo to use FX MIDI in games with your weapons.
Remember 'make love, not war' :mrgreen:

And happy 2024!! Hip!

Code: Select all

; ---- Por Minimy 2023/24 ----

Global salidaMIDI
Procedure startMidi()
  If midiOutOpen_(@salidaMIDI, 0, 0, 0, 0) = #MMSYSERR_NOERROR
    Debug "MIDI OK - POTATO NO DETECTED"
  Else
    MessageRequester("ERROR","MY OLD ATARI ST HAVE MIDI. Why you not?")
    End
  EndIf
EndProcedure
Procedure setMidiInstrument(canal=1, instrumento=127)
  Protected chain= ($C0 | canal) | (instrumento << 8);
  midiOutShortMsg_(salidaMIDI, chain)
EndProcedure
Procedure PlayMidiSound(canal=1, nota=60, volume=127, duracion=300, reset=0)
  Protected note= ($90 | canal) | (nota << 8) | (volume << 16);
  midiOutShortMsg_(salidaMIDI, note)
  Sleep_(duracion)
  If reset
    midiOutReset_(salidaMIDI)
  EndIf
EndProcedure

startMidi()
  setMidiInstrument(1,124)
    PlayMidiSound(1,60,127,1000, #True)
    Sleep_(1000)
    PlayMidiSound(1,60,127,1000, #True)
    Debug "No answer?"
  setMidiInstrument(1,125)
    PlayMidiSound(1,60,127,1000)
    Debug "Attack!"
  setMidiInstrument(1,127)
    PlayMidiSound(1,60,127,1000)
    PlayMidiSound(1,50,127,1000)
    Debug ";-)"
  For p=1 To 10
    PlayMidiSound(1,45,127,200)
  Next p
  Sleep_(1000)

midiOutReset_(salidaMIDI)
midiOutClose_(salidaMIDI)



Re: Halt Fire please!! (Midi Sound) [Windows]

Posted: Thu Dec 28, 2023 8:25 pm
by Kwai chang caine
Yeeees very cool !!!
Works great here 8)
But i haven't understand no one answers in my phone :shock: when i answer :lol:
Thanks for sharing 8)

Re: Halt Fire please!! (Midi Sound) [Windows]

Posted: Fri Dec 29, 2023 5:22 pm
by SPH
Thx, that's nice and lolant "(lolant" in french language, it's : HAHAAHHAHAHAHAAA) :mrgreen:

Re: Halt Fire please!! (Midi Sound) [Windows]

Posted: Fri Dec 29, 2023 10:07 pm
by minimy
Kwai chang caine wrote: Thu Dec 28, 2023 8:25 pm Yeeees very cool !!!
Works great here 8)
But i haven't understand no one answers in my phone :shock: when i answer :lol:
Thanks for sharing 8)
Thanks Kwai im really happy you answer!! :D
SPH wrote: Fri Dec 29, 2023 5:22 pm Thx, that's nice and lolant "(lolant" in french language, it's : HAHAAHHAHAHAHAAA) :mrgreen:
Thats the idea, easy code and fun. Thanks SPH!! :lol:

Happy new year guys!!
Be happy or the helicopter will shot you!! HAHAHA

Re: Halt Fire please!! (Midi Sound) [Windows]

Posted: Sat Jan 06, 2024 11:07 am
by pf shadoko
interesting
Does it work on all OS ?

Re: Halt Fire please!! (Midi Sound) [Windows]

Posted: Sat Jan 06, 2024 12:40 pm
by infratec
No.

the win api calls are not available in the other OS.

linux can play and create MIDI, but ... it's not plug'n play.
You need to install something like fluidsynth and a soundfont, then you can use alsa to play MIDI about it.

On macOS it should be easier.

Re: Halt Fire please!! (Midi Sound) [Windows]

Posted: Wed May 28, 2025 12:16 pm
by CDXbow
minimy, I made a midi file player starting with your code. It has been a long and much more difficult journey that I expected or should be. It opens and plays files and includes adjustable tempo. The problem is it only plays MIDI type 0 files. These are not common and it makes the whole thing pretty futile. If you wish to convert a file to type 0 you can use this tool http://www.jososoft.dk/yamaha/software/ ... tsetup.exe
The results can be idiosyncratic

The bpm adjustment only works while the song is paused or before it starts.
If it's a type 1 file it plays a scale.

Code: Select all

; Midi File Player - WINDOWS only
; By CD Xbow and CP.
; MIDIGUIPlayer2.pb 
; WARNING Only plays MIDI type 0 files
; Started with code by minimy - https://www.purebasic.fr/english/viewtopic.php?p=613600&hilit=midi#p613600

EnableExplicit

Global salidaMIDI, midiFileName.s = ""
Global playing = #False
Global paused = #False
Global stopped = #True
Global bpm = 120
Global playThread
Global noteIndex = 0
Global midiFileLoaded = #False
Global midiData$ = ""
Global midiPos = 0
Global midiTicksPerQuarter = 480 ; default, will be set from file
Global midiEventsCount = 0
Global Dim midiEvents.l(0)
Global midiEventIndex = 0
Global midiFileType = 0
Enumeration
  #Win
  #BtnOpen
  #BtnPlay
  #BtnPause
  #BtnStop
  #LblFile
  #TrackTempo
  #LblBPM
EndEnumeration

Procedure startMidi()
  If midiOutOpen_(@salidaMIDI, 0, 0, 0, 0) = #MMSYSERR_NOERROR
    ; OK
  Else
    MessageRequester("ERROR","No MIDI device found.")
    End
  EndIf
EndProcedure

Procedure setMidiInstrument(canal=0, instrumento=0)
  Protected chain = ($C0 | canal) | (instrumento << 8)
  midiOutShortMsg_(salidaMIDI, chain)
EndProcedure

Procedure SendMidiEvent(ev.l)
  midiOutShortMsg_(salidaMIDI, ev)
EndProcedure

Procedure PlayMidiNote(canal=0, nota=60, volume=127, duracion=300)
  Protected note = ($90 | canal) | (nota << 8) | (volume << 16)
  midiOutShortMsg_(salidaMIDI, note)
  Delay(duracion)
  ; Note Off (Note On with velocity 0)
  note = ($90 | canal) | (nota << 8)
  midiOutShortMsg_(salidaMIDI, note)
EndProcedure

Procedure.i ReadIntLE(*buf)
  ; Read 4 bytes, big endian to little endian
  Protected b1 = PeekA(*buf)
  Protected b2 = PeekA(*buf+1)
  Protected b3 = PeekA(*buf+2)
  Protected b4 = PeekA(*buf+3)
  ProcedureReturn (b1<<24) | (b2<<16) | (b3<<8) | b4
EndProcedure

Procedure.i ReadWordBE(*buf)
  ; Read 2 bytes, big endian
  ProcedureReturn (PeekA(*buf)<<8) | PeekA(*buf+1)
EndProcedure

Procedure.i ReadVarLen(*buf, *len.Integer)
  Protected val = 0
  Protected c, cnt = 0
  Repeat
    c = PeekA(*buf+cnt)
    val << 7
    val | (c & $7F)
    cnt + 1
  Until (c & $80) = 0
  *len\i = cnt
  ProcedureReturn val
EndProcedure

; --- Minimal MIDI file parser for Type 0 (single track) ---
Procedure ParseMidiFile(filename$)
  Protected filelen = FileSize(filename$)
  If filelen < 16 : ProcedureReturn 0 : EndIf
  Dim midiEvents.l(10000) ; Up to 10k events
  midiEventsCount = 0
  midiTicksPerQuarter = 480
  midiEventIndex = 0
  midiFileType = 0
  
  If ReadFile(0, filename$)
    Protected *buf = AllocateMemory(filelen)
    If *buf
      ReadData(0, *buf, filelen)
      CloseFile(0)
      
      ; Check header
      If PeekS(*buf, 4, #PB_Ascii) <> "MThd"
        FreeMemory(*buf)
        ProcedureReturn 0
      EndIf
      midiFileType = ReadWordBE(*buf+8)
      midiTicksPerQuarter = ReadWordBE(*buf+12)
      ; Only handle type 0 for now
      If midiFileType <> 0
        FreeMemory(*buf)
        ProcedureReturn 0
      EndIf
      ; Find track
      Protected trkpos = 14
      While trkpos < filelen-8
        If PeekS(*buf+trkpos, 4, #PB_Ascii) = "MTrk"
          Protected trackLen = ReadIntLE(*buf+trkpos+4)
          Protected p = trkpos + 8
          Protected trackEnd = p + trackLen
          Protected delta, status, dat1, dat2, ev, vlen, len, runningStatus=0
          Protected absTick = 0
          
          While p < trackEnd
            delta = ReadVarLen(*buf+p, @vlen)
            p + vlen
            absTick + delta
            status = PeekA(*buf+p)
            If status & $80
              runningStatus = status
              p + 1
            Else
              status = runningStatus
            EndIf
            If (status & $F0) = $90 ; Note On
              dat1 = PeekA(*buf+p)
              dat2 = PeekA(*buf+p+1)
              p + 2
              If dat2 > 0
                ; Pack: absTick (high word), status|dat1|dat2 (low word)
                midiEvents(midiEventsCount) = (absTick<<16) | (status<<8) | (dat1) | (dat2<<16)
                midiEventsCount + 1
              EndIf
            ElseIf (status & $F0) = $80 ; Note Off
              dat1 = PeekA(*buf+p)
              dat2 = PeekA(*buf+p+1)
              p + 2
              ; Treat as Note On with velocity 0
              midiEvents(midiEventsCount) = (absTick<<16) | ($90<<8) | (dat1)
              midiEventsCount + 1
            ElseIf (status & $F0) = $C0 ; Program Change
              dat1 = PeekA(*buf+p)
              p + 1
              ; Optional: handle instrument
            ElseIf status = $FF ; Meta event
              dat1 = PeekA(*buf+p)
              p + 1
              len = ReadVarLen(*buf+p, @vlen)
              p + vlen + len
            Else
              ; Skip unknowns (could be CC, etc)
              p + 2
            EndIf
            If midiEventsCount >= ArraySize(midiEvents())-2
              Break
            EndIf
          Wend
          Break ; Only first track
        EndIf
        trkpos + 1
      Wend
      FreeMemory(*buf)
      ReDim midiEvents(midiEventsCount)
      If midiEventsCount > 0
        ProcedureReturn 1
      EndIf
    EndIf
    CloseFile(0)
  EndIf
  ProcedureReturn 0
EndProcedure

Procedure PlayMidiThread(*dummy)
  Protected lastTick = 0, absTick, status, dat1, dat2, delay, i, interval
  Protected qn_ms = 60000/bpm
  While playing
    If paused Or stopped
      Delay(10)
      Continue
    EndIf
    If midiFileLoaded And midiEventsCount > 0
      ; Real MIDI file play
      For i = midiEventIndex To midiEventsCount-1
        absTick = midiEvents(i)>>16
        status = (midiEvents(i)>>8) & $FF
        dat1 = midiEvents(i) & $FF
        dat2 = (midiEvents(i)>>16) & $7F
        delay = (absTick - lastTick) * qn_ms / midiTicksPerQuarter
        If delay > 0 : Delay(delay) : EndIf
        If (status & $F0) = $90 ; Note On
          Protected msg = ($90 | (status & $0F)) | (dat1<<8) | (dat2<<16)
          midiOutShortMsg_(salidaMIDI, msg)
          ; For simplicity, send Note Off after fixed time
          Delay(qn_ms/4)
          msg = ($90 | (status & $0F)) | (dat1<<8)
          midiOutShortMsg_(salidaMIDI, msg)
        EndIf
        lastTick = absTick
        If paused Or stopped : Break : EndIf
      Next
      playing = #False
      stopped = #True
    Else
      ; Dummy notes
      interval = Int(60000 / bpm)
      PlayMidiNote(0, 60, 127, interval-20)
      PlayMidiNote(0, 64, 127, interval-20)
      PlayMidiNote(0, 67, 127, interval-20)
      PlayMidiNote(0, 72, 127, interval-20)
    EndIf
  Wend
EndProcedure

Procedure OpenMIDI()
  Protected file.s = OpenFileRequester("Select MIDI File", "", "MIDI files (*.mid)|*.mid", 0)
  If file
    midiFileName = file
    SetGadgetText(#LblFile, GetFilePart(file))
    midiFileLoaded = ParseMidiFile(file)
    midiEventIndex = 0
    stopped = #True
    paused = #False
    playing = #False
    SetGadgetText(#BtnPlay, "Play")
  EndIf
EndProcedure

Procedure Play()
  If Not playing
    playing = #True
    paused = #False
    stopped = #False
    SetGadgetText(#BtnPlay, "Playing...")
    playThread = CreateThread(@PlayMidiThread(), 0)
  ElseIf paused
    paused = #False
    SetGadgetText(#BtnPlay, "Playing...")
  EndIf
EndProcedure

Procedure Pause()
  If playing
    paused = #True
    SetGadgetText(#BtnPlay, "Play")
  EndIf
EndProcedure

Procedure Stop()
  If playing
    stopped = #True
    paused = #False
    playing = #False
    midiEventIndex = 0
    SetGadgetText(#BtnPlay, "Play")
    Delay(50)
    If playThread
      KillThread(playThread)
      playThread = 0
    EndIf
    midiOutReset_(salidaMIDI)
  EndIf
EndProcedure

Procedure ChangeTempo()
  bpm = GetGadgetState(#TrackTempo)
  SetGadgetText(#LblBPM, "BPM: " + Str(bpm))
EndProcedure

; --- MAIN ---
OpenWindow(#Win, 0, 0, 350, 180, "MIDI GUI Player", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
ButtonGadget(#BtnOpen, 10, 10, 80, 28, "Open MIDI")
TextGadget(#LblFile, 100, 16, 230, 24, "[No file selected]")
ButtonGadget(#BtnPlay, 10, 50, 60, 28, "Play")
ButtonGadget(#BtnPause, 80, 50, 60, 28, "Pause")
ButtonGadget(#BtnStop, 150, 50, 60, 28, "Stop")
TextGadget(#LblBPM, 230, 60, 80, 24, "BPM: 120")
TrackBarGadget(#TrackTempo, 10, 100, 320, 25, 40, 240)
SetGadgetState(#TrackTempo, 120)

startMidi()
setMidiInstrument(0,0) ; Piano

Repeat
  Select WaitWindowEvent(10)
    Case #PB_Event_CloseWindow
      Stop()
      Break
    Case #PB_Event_Gadget
      Select EventGadget()
        Case #BtnOpen
          OpenMIDI()
        Case #BtnPlay
          Play()
        Case #BtnPause
          Pause()
        Case #BtnStop
          Stop()
        Case #TrackTempo
          ChangeTempo()
      EndSelect
  EndSelect
ForEver

Stop()
midiOutClose_(salidaMIDI)
A good learning experience or a painful waste of time. I'm still deciding.

I wanted to try portmidi which handles type 1 midi files and allows much more control. Unfortunately the only 'alleged' 64 bit DLL I could find was from https://www.dllme.com/dll/files/portmid ... 05aa06587d I am always filled with dread when I have to use a DLL of uncertain provenance. The only alternative is to compile a 64bit DLL. Me and C often don't agree, so I haven't decided what to do yet.

Re: Halt Fire please!! (Midi Sound) [Windows]

Posted: Wed May 28, 2025 2:03 pm
by minimy
Hello CDXbow! very interesting.
Im not using midi because cant play together wav and midi. Idk if is sound card limitation or is a sofware limitation.
Im working now with wav or mp3 is more easy for me. :D

Re: Halt Fire please!! (Midi Sound) [Windows]

Posted: Wed May 28, 2025 2:14 pm
by Quin
minimy wrote: Wed May 28, 2025 2:03 pm Im not using midi because cant play together wav and midi. Idk if is sound card limitation or is a sofware limitation.
I'd think that's a soundcard limitation, most modern soundcards should have no problem outputting multiple channels of different sources.