VBAでwavを鳴らしたら珍現象が起きた話

VBAでwavを鳴らしたらち~んw珍現象が起きた

解決方法を知っている人がいたら、教えろ教えてください。

KitchenTimerクラス

前に、

akashi-keirin.hatenablog.com

こういうものを作ったら、職場で割とウケたので、「残り時間を表示するカウントダウンタイマーみてえなやつを作ったら面白えんじゃね?」と思って作ってみた。

f:id:akashi_keirin:20190923084747g:plain

……んだけど、ソースコードを職場に置いてきてしまったので、イチから書き直しw

ただ書き直すだけではおもんないんで、無駄にクラス化してみた。

クラスモジュール KitchenTimer
Option Explicit

'Win32API Declaration'
'For Calc Time'
Private Declare Function GetTickCount Lib "kernel32" () As Long
Private Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)

'For Play Sound'
Private Declare Function mciSendString _
                  Lib "winmm.dll" _
                  Alias "mciSendStringA" ( _
                           ByVal lpstrCommand As String, _
                           ByVal lpstrReturnString As String, _
                           ByVal uReturnLength As Long, _
                           ByVal hwndCallback As Long) As Long

'Constants'
Private Enum TimerStatus
  tsRemaining
  tsPaused
  tsFinished
End Enum

'Module Level Variables'
Private fsObj As New FileSystemObject

'Module Level Variables; Fields'
Private minutes As Long
Private seconds As Long
Private soundSrcPath_ As String
Private totalSeconds As Long
Private status_ As TimerStatus

Private minDisp As Range
Private secDisp As Range
Private statDisp As Range

Private isInitialized As Boolean

Private isPaused As Boolean

Private Sub Class_Initialize()
  totalSeconds = 0
  isPaused = True
  isInitialized = False
End Sub

'Properties'
Private Property Get Status() As String
  Dim ret As String
  Select Case status_
    Case tsRemaining: ret = "あと"
    Case tsPaused: ret = "一時停止"
    Case tsFinished: ret = "終了!"
  End Select
  Status = ret
End Property

Public Property Get HasDone() As Boolean
  HasDone = True
  If status_ = tsFinished Then Exit Property
  HasDone = False
End Property

'Constructor'
Public Sub init(ByVal minDisplay As Range, _
                ByVal secDisplay As Range, _
                ByVal statDisplay As Range, _
       Optional ByVal soundSrcPath As String)
  Set minDisp = minDisplay
  Set secDisp = secDisplay
  Set statDisp = statDisplay
  If soundSrcPath <> "" Then
    If Not fsObj.FileExists(soundSrcPath) Or _
       StrConv(Right(soundSrcPath, 4), vbLowerCase) <> ".wav" Then _
      soundSrcPath = ""
  End If
  soundSrcPath_ = soundSrcPath
  isInitialized = True
End Sub

'Methods'
Public Sub countDown( _
    Optional ByVal milliSecPerSec As Long = 1000)
  If Not isInitialized Then Exit Sub
  isPaused = False
  minutes = minDisp.Value
  If minutes < 0 Then minutes = 0
  seconds = secDisp.Value
  If seconds < 0 Then seconds = 0
  If seconds > 59 Then seconds = 0
  totalSeconds = minutes * 60 + seconds
  If minutes + seconds = 0 Then Exit Sub
  Dim interval As Long
  interval = milliSecPerSec - 1
  status_ = tsRemaining
  statDisp.Value = Status
  Do While totalSeconds > 0
    If isPaused Then _
      status_ = tsPaused: statDisp.Value = Status: _
      Exit Do
    Call waitFor(interval)
    totalSeconds = totalSeconds - 1
    minutes = totalSeconds \ 60
    minDisp.Value = minutes
    seconds = totalSeconds Mod 60
    secDisp.Value = seconds
  Loop
  DoEvents
  If totalSeconds > 0 Then status_ = tsPaused
  If totalSeconds = 0 Then
    status_ = tsFinished
    statDisp.Value = Status
  End If
End Sub

Public Sub pause()
  isPaused = Not isPaused
End Sub

Public Sub reset()
  isPaused = True
  totalSeconds = 0
  minutes = 0
  minDisp.Value = minutes
  seconds = 0
  secDisp.Value = seconds
  status_ = tsFinished: statDisp.Value = Status
End Sub

'Internal Methods'
Private Function callGetTickCount() As Long
  Dim ret As Variant
  ret = GetTickCount()
  ret = CDec(ret)
  If ret < 0 Then ret = ret + 2 ^ 32
  callGetTickCount = ret
End Function

Private Sub callSleep(ByVal milliSeconds As Long)
  Call Sleep(milliSeconds)
End Sub

Private Sub waitFor(ByVal milliSeconds As Long)
  Dim startTime As Long
  startTime = callGetTickCount()
  Dim endTime As Long
  Do
    Call Sleep(1)
    DoEvents
    endTime = callGetTickCount()
  Loop Until endTime - startTime > milliSeconds
End Sub

Public Sub playSound()
  If soundSrcPath_ = "" Then Exit Sub
  Dim rc As Long
  soundSrcPath_ = """" & soundSrcPath_ & """"
  rc = mciSendString("Open " & soundSrcPath_, "", 0, 0)
  rc = mciSendString("Play " & soundSrcPath_ & " wait", "", 0, 0)
  rc = mciSendString("Close " & soundSrcPath_, "", 0, 0)
  status_ = tsPaused
End Sub

※上掲コードはご覧のとおり32bit版Officeのものです。64bit版をご利用の方は、しかるべく修正してお使いください。

まあ、64ビット対応っつってもこの場合だったらPtrSafe入れるだけでいいと思うけど。

擬似コンストラクinitメソッドの冒頭を

Public Sub init(ByVal minDisplay As Range, _
                ByVal secDisplay As Range, _
                ByVal statDisplay As Range, _
       Optional ByVal soundSrcPath As String)

このようにしている。

引数が計四つ。

第1引数minDisplayには、「分」数を表示させるセルを指定。第2引数secDisplayには、「秒」数を表示するセルを指定。第3引数statDisplayには、カウントダウンの状態を表示するセルを指定。あと、省略可能の第4引数soundSrcPathには、カウントダウン終了後に鳴らすwavファイルのフルパスを指定することにしている。

外部に公開するメソッドは「countDown」、「pause」、「reset」の三つ。

何をするメソッドなのかは、名前を見たらわかるだろう。まさにキッチンタイマーを操作するときのアレとかアレである。

使ってみる

たとえば、ワークシート上に

f:id:akashi_keirin:20190923084614j:plain

このようにキッチンタイマー風の外観を作成しておき、次のようなコードで利用する。

リスト1
Private Sub testKitchenTimer()
  Dim kitTimer As KitchenTimer  '……(1)'
  Set kitTimer = New KitchenTimer
  Call kitTimer.init(minDisplay:=Sh01Main.Range("B2"), _
                     secDisplay:=Sh01Main.Range("D2"), _
                     statDisplay:=Sh01Main.Range("A1"), _
                     soundSrcPath:=ThisWorkbook.Path & _
                                   "\SoundSrc\GOLDEN AXE Voice Yell.wav")
  Call kitTimer.countDown(Sh01Main.Range("G1").Value)    '……(2)'
  Call kitTimer.playSound    '……(3)'
End Sub

(1)の

Dim kitTimer As KitchenTimer
Set kitTimer = New KitchenTimer
Call kitTimer.init(minDisplay:=Sh01Main.Range("B2"), _
                   secDisplay:=Sh01Main.Range("D2"), _
                   statDisplay:=Sh01Main.Range("A1"), _
                   soundSrcPath:=ThisWorkbook.Path & _
                                 "\SoundSrc\GOLDEN AXE Voice Yell.wav")

インスタンス生成。引数祭りで申しわけないが、

  • 「分」数表示セルはB2セル
  • 「秒」数表示セルはD2セル
  • 状態を表示するセルはA1セル
  • カウントダウン終了時に鳴らすwavファイルは、このプロジェクトのあるフォルダの中の「SoundSrc」フォルダ内の「GOLDEN AXE Voice Yell.wav

とまあ、こんな感じの設定。

画面は

f:id:akashi_keirin:20190923084614j:plain

こんなの。

G1セルの「333」というのは、カウントダウンの1秒を何ミリ秒にするか、という数値。

倍速でカウントダウンさせたければ、「500」にすれば良い。

目分量だが、「850」ぐらいまでならバレないw

画面では「333」にしているので、約3倍速。

(2)の

Call kitTimer.countDown(Sh01Main.Range("G1").Value)

のように、countDownメソッドの引数として渡してやる。

カウントダウンが終わったら、(3)の

Call kitTimer.playSound

でwavを再生して終わり。

ちなみに、画面ではコマンドボタンを置いているが、今回はcountDownplaySoundメソッドしか使わないので、ボタンの存在は無視してください。

ち~んw珍現象

上記リスト1を実行すると、カウントダウン終了→wavファイル鳴動、となる……はずである。ところが、

f:id:akashi_keirin:20190923084713g:plain

音が鳴らないのでわかりにくいかも知れないが、「あと0分01秒」で止まっているのがわかるだろうか。

止まっている間にwavが鳴っているのである。

コードの実行順から考えると、表示が「0分00秒」になる→wavが鳴る、の順にならねばならんはずなのに。

おわりに

ちなみに、リスト1の(3)の行にブレークポイントを置いて実行すると、ちゃんと「0分00秒」になってから止まる。

なのに、通し実行すると「0分01秒」のところで一旦音が鳴り、鳴り終わってから「0分00秒」になるのである。

何ででしょう???

誰かわかる人いたら教えろ教えてください。