Code: Select all
EnableExplicit
; ==========================================================
; Microphone Recorder (WAV) – PureBasic 6.30 (WinMM waveIn*)
; Features:
; - Microphone selection
; - Level meter
; - Software gain (0..4x)
; - Auto-pause after 5s silence, resume on voice + 300ms pre-roll
; ==========================================================
CompilerIf #PB_Compiler_OS <> #PB_OS_Windows
MessageRequester("Not supported", "This example is Windows-only (WinMM waveIn*).", 0)
End
CompilerEndIf
#SAMPLE_RATE = 44100
#CHANNELS = 1
#BITS = 16
#NUM_BUFFERS = 4
#BUFFER_BYTES = 8192
#SILENCE_MAX_MS = 5000
#PREROLL_MS = 300
#QUEUE_SLOTS = 128
#WAVE_MAPPER = -1
#CALLBACK_WINDOW = $00010000
#WAVE_FORMAT_PCM = 1
#MM_WIM_OPEN = $3BE
#MM_WIM_CLOSE = $3BF
#MM_WIM_DATA = $3C0
#MAXPNAMELEN = 32
Import "winmm.lib"
waveInOpen.l(*phwi, uDeviceID.i, *pwfx.WAVEFORMATEX, dwCallback.i, dwInstance.i, fdwOpen.l)
waveInPrepareHeader.l(hwi.i, *pwh.WAVEHDR, cbwh.l)
waveInUnprepareHeader.l(hwi.i, *pwh.WAVEHDR, cbwh.l)
waveInAddBuffer.l(hwi.i, *pwh.WAVEHDR, cbwh.l)
waveInStart.l(hwi.i)
waveInStop.l(hwi.i)
waveInReset.l(hwi.i)
waveInClose.l(hwi.i)
waveInGetNumDevs.l()
waveInGetDevCapsW.l(uDeviceID.i, *pwic.WAVEINCAPS, cbwic.l)
EndImport
; ---------- Globals ----------
Global gWaveIn.i
Global gCapturing.i
Global gShuttingDown.i
Global gSelectedDeviceID.i = #WAVE_MAPPER
Global gGainPermille.l = 1000 ; 1000 = 1.0x
Global gVoiceThreshold.l = 600
Global gMeterPermille.l = 0
Global gAutoPaused.l = 0
Global gDropped.l = 0
Global gFile.i = -1
Global gDataBytes.q
Global gFileName.s
Global gStatus.s = "Ready"
Global gHwndMain.i
Global Dim gHdr.WAVEHDR(#NUM_BUFFERS-1)
Global Dim *gBuf(#NUM_BUFFERS-1)
; ---------- Queue (Window thread -> Worker thread) ----------
Global *gQueuePool = AllocateMemory(#QUEUE_SLOTS * #BUFFER_BYTES)
Global Dim gQueueBytes.i(#QUEUE_SLOTS-1)
Global gQRead.i, gQWrite.i, gQCount.i
Global gQMutex.i = CreateMutex()
Global gQSem.i = CreateSemaphore()
Global gWorkerThread.i
Global gWorkerQuit.i
; ---------- Worker buffers ----------
Global gSilenceBufBytes.i = (#SAMPLE_RATE * #CHANNELS * (#BITS/8) * #SILENCE_MAX_MS) / 1000
Global *gSilenceBuf = AllocateMemory(gSilenceBufBytes)
Global gPreRollBytes.i = (#SAMPLE_RATE * #CHANNELS * (#BITS/8) * #PREROLL_MS) / 1000
Global *gPreRoll = AllocateMemory(gPreRollBytes)
; ---------- UI ----------
Enumeration
#WinMain
#cmbDevice
#btnRefresh
#btnStart
#btnStop
#txtFile
#trkGain
#lblGain
#trkThresh
#lblThresh
#prgLevel
#lblState
#tmrUI
EndEnumeration
; ---------- WAV header ----------
Procedure WriteWavHeader(File.i, DataBytes.q)
Protected channels = #CHANNELS
Protected sr = #SAMPLE_RATE
Protected bps = #BITS
Protected byteRate = sr * channels * (bps/8)
Protected blockAlign = channels * (bps/8)
FileSeek(File, 0)
WriteString(File, "RIFF", #PB_Ascii)
WriteLong(File, 36 + DataBytes)
WriteString(File, "WAVE", #PB_Ascii)
WriteString(File, "fmt ", #PB_Ascii)
WriteLong(File, 16)
WriteWord(File, #WAVE_FORMAT_PCM)
WriteWord(File, channels)
WriteLong(File, sr)
WriteLong(File, byteRate)
WriteWord(File, blockAlign)
WriteWord(File, bps)
WriteString(File, "data", #PB_Ascii)
WriteLong(File, DataBytes)
EndProcedure
; ---------- Audio helpers ----------
Procedure Clamp16(v.l)
If v > 32767 : ProcedureReturn 32767 : EndIf
If v < -32768 : ProcedureReturn -32768 : EndIf
ProcedureReturn v
EndProcedure
Procedure ApplyGainAndMeter(*buf, bytes.i, gainPermille.i, *avgAbs.Integer, *peak.Integer)
Protected samples.i = bytes / 2
Protected i.i, s.l, a.l
Protected sumAbs.q
Protected peakLocal.l
If samples <= 0
*avgAbs\i = 0 : *peak\i = 0 : ProcedureReturn
EndIf
If gainPermille = 1000
For i = 0 To samples - 1
s = PeekW(*buf + i*2)
a = s : If a < 0 : a = -a : EndIf
sumAbs + a
If a > peakLocal : peakLocal = a : EndIf
Next
Else
For i = 0 To samples - 1
s = PeekW(*buf + i*2)
s = Clamp16((s * gainPermille) / 1000)
PokeW(*buf + i*2, s)
a = s : If a < 0 : a = -a : EndIf
sumAbs + a
If a > peakLocal : peakLocal = a : EndIf
Next
EndIf
*avgAbs\i = sumAbs / samples
*peak\i = peakLocal
EndProcedure
; ---------- Pre-roll ring (Worker only) ----------
Procedure PreRollPush_Worker(*pre, preBytes.i, *buf, bytes.i, *pos.Integer, *filled.Integer)
Protected first.i
If bytes <= 0 Or preBytes <= 0 : ProcedureReturn : EndIf
If bytes >= preBytes
CopyMemory(*buf + bytes - preBytes, *pre, preBytes)
*pos\i = 0
*filled\i = preBytes
ProcedureReturn
EndIf
first = preBytes - *pos\i
If bytes <= first
CopyMemory(*buf, *pre + *pos\i, bytes)
*pos\i + bytes
If *pos\i = preBytes : *pos\i = 0 : EndIf
Else
CopyMemory(*buf, *pre + *pos\i, first)
CopyMemory(*buf + first, *pre, bytes - first)
*pos\i = bytes - first
EndIf
*filled\i + bytes
If *filled\i > preBytes : *filled\i = preBytes : EndIf
EndProcedure
Procedure PreRollWrite_Worker(File.i, *pre, preBytes.i, pos.i, filled.i)
Protected tail.i
If filled <= 0 Or File < 0 : ProcedureReturn : EndIf
If filled < preBytes
WriteData(File, *pre, filled)
gDataBytes + filled
Else
tail = preBytes - pos
WriteData(File, *pre + pos, tail) : gDataBytes + tail
If pos > 0
WriteData(File, *pre, pos) : gDataBytes + pos
EndIf
EndIf
EndProcedure
; ---------- Worker thread ----------
Procedure WorkerThread(*dummy)
Protected *work = AllocateMemory(#BUFFER_BYTES)
Protected bytes.i, idx.i, avgAbs.Integer, peak.Integer
Protected gainP.i, thresh.i, meter.i
Protected autoPaused.i = 0
Protected silenceUsed.i = 0
Protected prePos.Integer : prePos\i = 0
Protected preFilled.Integer : preFilled\i = 0
While #True
WaitSemaphore(gQSem)
While #True
If gWorkerQuit And gQCount = 0
FreeMemory(*work)
ProcedureReturn
EndIf
LockMutex(gQMutex)
If gQCount = 0
UnlockMutex(gQMutex)
Break
EndIf
idx = gQRead
bytes = gQueueBytes(idx)
If bytes > #BUFFER_BYTES : bytes = #BUFFER_BYTES : EndIf
CopyMemory(*gQueuePool + idx * #BUFFER_BYTES, *work, bytes)
gQRead + 1 : If gQRead = #QUEUE_SLOTS : gQRead = 0 : EndIf
gQCount - 1
UnlockMutex(gQMutex)
gainP = gGainPermille
thresh = gVoiceThreshold
ApplyGainAndMeter(*work, bytes, gainP, @avgAbs, @peak)
PreRollPush_Worker(*gPreRoll, gPreRollBytes, *work, bytes, @prePos, @preFilled)
meter = (peak\i * 1000) / 32767
gMeterPermille = meter
If gFile >= 0
If avgAbs\i >= thresh
If autoPaused
autoPaused = 0
silenceUsed = 0
PreRollWrite_Worker(gFile, *gPreRoll, gPreRollBytes, prePos\i, preFilled\i)
ElseIf silenceUsed > 0
WriteData(gFile, *gSilenceBuf, silenceUsed) : gDataBytes + silenceUsed
silenceUsed = 0
EndIf
WriteData(gFile, *work, bytes) : gDataBytes + bytes
Else
If autoPaused = 0
If silenceUsed + bytes < gSilenceBufBytes
CopyMemory(*work, *gSilenceBuf + silenceUsed, bytes)
silenceUsed + bytes
Else
silenceUsed = 0
autoPaused = 1
EndIf
EndIf
EndIf
EndIf
gAutoPaused = autoPaused
Wend
Wend
EndProcedure
; ---------- Window callback: receive MM_WIM_* messages ----------
Procedure WinMsgCB(WindowID, Message, wParam, lParam)
Protected result = #PB_ProcessPureBasicEvents
Protected *hdr.WAVEHDR
Protected bytes.i
Select Message
Case #MM_WIM_OPEN
gStatus = "Input device opened"
ProcedureReturn result
Case #MM_WIM_CLOSE
gStatus = "Input device closed"
ProcedureReturn result
Case #MM_WIM_DATA
If gShuttingDown Or gCapturing = 0
ProcedureReturn result
EndIf
*hdr = lParam
bytes = *hdr\dwBytesRecorded
; Safety clamps
If bytes < 0 : bytes = 0 : EndIf
If bytes > *hdr\dwBufferLength : bytes = *hdr\dwBufferLength : EndIf
If bytes > #BUFFER_BYTES : bytes = #BUFFER_BYTES : EndIf
If bytes > 0
LockMutex(gQMutex)
If gQCount < #QUEUE_SLOTS
CopyMemory(*hdr\lpData, *gQueuePool + gQWrite * #BUFFER_BYTES, bytes)
gQueueBytes(gQWrite) = bytes
gQWrite + 1 : If gQWrite = #QUEUE_SLOTS : gQWrite = 0 : EndIf
gQCount + 1
UnlockMutex(gQMutex)
SignalSemaphore(gQSem)
Else
UnlockMutex(gQMutex)
gDropped + 1
EndIf
EndIf
; Return buffer to driver
waveInAddBuffer(wParam, *hdr, SizeOf(WAVEHDR))
ProcedureReturn result
EndSelect
ProcedureReturn result
EndProcedure
; ---------- Devices ----------
Procedure PopulateDevices()
Protected n.i, i.i, caps.WAVEINCAPS, name.s
ClearGadgetItems(#cmbDevice)
AddGadgetItem(#cmbDevice, -1, "Default device (Windows)")
SetGadgetItemData(#cmbDevice, 0, #WAVE_MAPPER)
n = waveInGetNumDevs()
For i = 0 To n - 1
If waveInGetDevCapsW(i, @caps, SizeOf(WAVEINCAPS)) = 0
name = PeekS(@caps\szPname[0], -1, #PB_Unicode)
AddGadgetItem(#cmbDevice, -1, name)
SetGadgetItemData(#cmbDevice, CountGadgetItems(#cmbDevice)-1, i)
EndIf
Next
SetGadgetState(#cmbDevice, 0)
DisableGadget(#btnStart, Bool(CountGadgetItems(#cmbDevice) = 1))
EndProcedure
; ---------- Start/Stop ----------
Procedure StartRecording()
Protected fmt.WAVEFORMATEX
Protected i.i, res.l
Protected sel.i, devId.i
If gCapturing : ProcedureReturn : EndIf
sel = GetGadgetState(#cmbDevice)
If sel < 0 : sel = 0 : EndIf
devId = GetGadgetItemData(#cmbDevice, sel)
gSelectedDeviceID = devId
gFileName = SaveFileRequester("Save WAV", "recording.wav", "WAV (*.wav)|*.wav", 0)
If gFileName = "" : ProcedureReturn : EndIf
gFile = CreateFile(#PB_Any, gFileName)
If gFile = 0
gFile = -1
MessageRequester("Error", "Could not create output file.", 0)
ProcedureReturn
EndIf
gDataBytes = 0
WriteWavHeader(gFile, 0)
; Queue reset
LockMutex(gQMutex)
gQRead = 0 : gQWrite = 0 : gQCount = 0
UnlockMutex(gQMutex)
gDropped = 0
gAutoPaused = 0
gMeterPermille = 0
; Worker start
gWorkerQuit = 0
gWorkerThread = CreateThread(@WorkerThread(), 0)
; waveIn open (CALLBACK_WINDOW -> messages to our window)
fmt\wFormatTag = #WAVE_FORMAT_PCM
fmt\nChannels = #CHANNELS
fmt\nSamplesPerSec = #SAMPLE_RATE
fmt\wBitsPerSample = #BITS
fmt\nBlockAlign = (#CHANNELS * (#BITS/8))
fmt\nAvgBytesPerSec = fmt\nSamplesPerSec * fmt\nBlockAlign
fmt\cbSize = 0
gShuttingDown = 0
res = waveInOpen(@gWaveIn, devId, @fmt, gHwndMain, 0, #CALLBACK_WINDOW)
If res <> 0
gWorkerQuit = 1 : SignalSemaphore(gQSem) : WaitThread(gWorkerThread)
CloseFile(gFile) : gFile = -1
MessageRequester("Error", "waveInOpen() failed (code: " + Str(res) + ")", 0)
ProcedureReturn
EndIf
For i = 0 To #NUM_BUFFERS-1
*gBuf(i) = AllocateMemory(#BUFFER_BYTES)
FillMemory(*gBuf(i), #BUFFER_BYTES, 0)
gHdr(i)\lpData = *gBuf(i)
gHdr(i)\dwBufferLength = #BUFFER_BYTES
gHdr(i)\dwBytesRecorded = 0
gHdr(i)\dwFlags = 0
gHdr(i)\dwLoops = 0
waveInPrepareHeader(gWaveIn, @gHdr(i), SizeOf(WAVEHDR))
waveInAddBuffer(gWaveIn, @gHdr(i), SizeOf(WAVEHDR))
Next
gCapturing = #True
waveInStart(gWaveIn)
gStatus = "Recording..."
DisableGadget(#btnStart, #True)
DisableGadget(#btnStop, #False)
DisableGadget(#cmbDevice, #True)
DisableGadget(#btnRefresh, #True)
EndProcedure
Procedure StopRecording()
Protected i.i
If gCapturing = 0 : ProcedureReturn : EndIf
gShuttingDown = 1
gCapturing = 0
waveInReset(gWaveIn)
waveInStop(gWaveIn)
For i = 0 To #NUM_BUFFERS-1
waveInUnprepareHeader(gWaveIn, @gHdr(i), SizeOf(WAVEHDR))
Next
waveInClose(gWaveIn)
gWaveIn = 0
For i = 0 To #NUM_BUFFERS-1
If *gBuf(i) : FreeMemory(*gBuf(i)) : *gBuf(i) = 0 : EndIf
Next
gWorkerQuit = 1
SignalSemaphore(gQSem)
WaitThread(gWorkerThread)
If gFile >= 0
WriteWavHeader(gFile, gDataBytes)
CloseFile(gFile)
gFile = -1
EndIf
gStatus = "Saved: " + gFileName + " (Dropped: " + Str(gDropped) + ")"
DisableGadget(#btnStart, #False)
DisableGadget(#btnStop, #True)
DisableGadget(#cmbDevice, #False)
DisableGadget(#btnRefresh, #False)
EndProcedure
Procedure UpdateUI()
SetGadgetState(#prgLevel, gMeterPermille)
SetGadgetText(#lblGain, "Input volume (software gain): " + StrF(gGainPermille/1000.0, 2) + "x")
SetGadgetText(#lblThresh, "Voice threshold: " + Str(gVoiceThreshold))
If gFileName = "" : SetGadgetText(#txtFile, "File: -") : Else : SetGadgetText(#txtFile, "File: " + gFileName) : EndIf
If gCapturing
If gAutoPaused
SetGadgetText(#lblState, "Status: AUTO-PAUSE (silence ≥ " + Str(#SILENCE_MAX_MS/1000) + "s) (Queue=" + Str(gQCount) + ", Dropped=" + Str(gDropped) + ")")
Else
SetGadgetText(#lblState, "Status: Recording... (Queue=" + Str(gQCount) + ", Dropped=" + Str(gDropped) + ")")
EndIf
Else
SetGadgetText(#lblState, "Status: " + gStatus)
EndIf
EndProcedure
; ---------- Main ----------
OpenWindow(#WinMain, 0, 0, 580, 300, "Microphone Recorder (WAV) – Auto Pause", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
gHwndMain = WindowID(#WinMain)
SetWindowCallback(@WinMsgCB(), #WinMain)
ComboBoxGadget(#cmbDevice, 20, 16, 390, 24)
ButtonGadget(#btnRefresh, 422, 14, 138, 28, "Rescan mics")
ButtonGadget(#btnStart, 20, 50, 120, 30, "Start")
ButtonGadget(#btnStop, 160, 50, 120, 30, "Stop")
DisableGadget(#btnStop, #True)
TextGadget(#txtFile, 20, 85, 540, 20, "File: -")
TextGadget(#lblGain, 20, 110, 540, 20, "Input volume (software gain): 1.00x")
TrackBarGadget(#trkGain, 20, 130, 540, 20, 0, 400)
SetGadgetState(#trkGain, 100)
TextGadget(#lblThresh, 20, 160, 540, 20, "Voice threshold: 600")
TrackBarGadget(#trkThresh, 20, 180, 540, 20, 0, 4000)
SetGadgetState(#trkThresh, gVoiceThreshold)
ProgressBarGadget(#prgLevel, 20, 210, 540, 18, 0, 1000)
TextGadget(#lblState, 20, 232, 540, 50, "Status: Ready")
PopulateDevices()
AddWindowTimer(#WinMain, #tmrUI, 50)
Repeat
Select WaitWindowEvent()
Case #PB_Event_CloseWindow
If gCapturing : StopRecording() : EndIf
Break
Case #PB_Event_Timer
If EventTimer() = #tmrUI : UpdateUI() : EndIf
Case #PB_Event_Gadget
Select EventGadget()
Case #btnRefresh
If gCapturing = 0 : PopulateDevices() : EndIf
Case #btnStart
StartRecording()
Case #btnStop
StopRecording()
Case #trkGain
gGainPermille = GetGadgetState(#trkGain) * 10 ; 0..4000
Case #trkThresh
gVoiceThreshold = GetGadgetState(#trkThresh)
EndSelect
EndSelect
ForEver