Creating an HTML Application to Convert Text Files to Audio Files

I'd like to take a brief departure from my normal collage of web-related and server-management examples and share a rather eclectic code sample.

Here's the scenario: I am presently working on another college degree, and I was recently taking a class which required a great deal of reading.  These assignments were all in digital form: I was using PDF or Kindle-based versions of the textbooks, and the remaining reading consisted of online articles. However, I am also an avid bicyclist, and the voluminous amount of reading was preventing me from going on some of my normal weekly rides. This gave me an idea: if I could convert the digital text to audio, I could bring my assignments with me on my longer rides and have my MP3 player read my assignments to me as I pedaled my way around the Arizona deserts.

I had experimented with Microsoft's built-in text-to-speech features some years ago, so I thought that this would be the perfect opportunity to revisit some of those APIs and write a full application to do the conversions for me. That being said, I could have written this application by using C# (which is my preferred language), but I decided to create an HTML Application for two reasons: 1) it is easier for me to share HTMLA applications in a blog, and 2) I keep this application in my OneDrive, so it's easier for me to modify the source code on systems where I don't have Visual Studio installed.

With that in mind, here is the resulting script which will convert a Text File to a Wave (*.WAV) File.

Using the HTML Application

As with most of my HTML Applications, the user interface is pretty simple to use; when you double click the HTA file, it will present you with the following user interface:

Clicking the Browse button will obviously allow you to browse to a text file, clicking the Close button will close the application, and clicking the Write File button will create a Wave file that is in the same path as the source text file. For example, if you have a text file at "C:\Text\Test.txt", the script will create a Wave file at "C:\Text\Test.txt.wav".

That being said, there are a few options which you can set:

  • Depending on your version of Windows and which languages you have installed, you will be presented with a list of available voices in the first drop-down menu; the following example is from a Windows 8 computer:
  • The second drop-down menu allows you to vary the playback speed; sometimes I prefer the playback speed to be slighter faster than normal:
  • The third drop-down menu allows you to alter the volume of the resulting Wave file:

Note: You can modify the script to alter the values that are used in the playback speed and volume drop-down menus, but the list of voices is obtained dynamically from your operating system.

Creating the HTML Application

To create this HTML Application, save the following HTMLA code as "Text to Wave File.hta" to your computer, and then double-click its icon to run the application. (Note that in a few places I added code comments which contain the MSDN URL for the APIs that I am using for this sample.)

<html>
<head>
<title>Text-to-Speech Writer</title>
<HTA:APPLICATION
  APPLICATIONNAME="Text-to-Speech Writer"
  ID="TextToSpeech"
  VERSION="1.0"
  BORDER="dialog"
  BORDERSTYLE="static"
  INNERBORDER="no"
  CAPTION="yes"
  SYSMENU="no"
  MAXIMIZEBUTTON="no"
  MINIMIZEBUTTON="no"
  SCROLL="no"
  SCROLLFLAT="yes"
  SINGLEINSTANCE="yes"
  CONTEXTMENU="no"
  SELECTION="no"/>
</head>

<script language="VBScript">

Option Explicit

Dim blnCancelBubble

' ----------------------------------------
' 
' OnLoad event handler for the application.
' 
' ----------------------------------------

Sub Window_OnLoad
  blnCancelBubble = False
  ' Set up the UI dimensions.
  Const intDialogWidth = 550
  Const intDialogHeight = 125
  ' Specify the window position and size.
  Self.resizeTo intDialogWidth,intDialogHeight
  Self.moveTo (Screen.AvailWidth - intDialogWidth) / 2,_
    (Screen.AvailHeight - intDialogHeight) / 2
  ' Load the list of text-to-speech voices into the drop-down menu.
  ' See the notes in WriteFile() for more information.
  Dim objSAPI, objVoice, objSelect, objOption
  Set objSAPI = CreateObject("SAPI.SpVoice")
  For Each objVoice In objSAPI.GetVoices("","")
    Set objSelect = Document.getElementById("optVoices")
    Set objOption = Document.createElement("option")
    objOption.text = objVoice.GetDescription() 
    objSelect.Add objOption
  Next
End Sub

' ----------------------------------------
' 
' Click handler for the Write button.
' 
' ----------------------------------------

Sub btnWrite_OnClick()
  ' Test for a file name.
  If Len(txtFile.Value) > 0 Then
    ' Test if we need to cancel bubbling of events.
    If blnCancelBubble = False Then
        ' Write the input file.
        Call WriteFile(txtFile.Value)
      End If
    End If
    ' Specify whether to bubble events.
    blnCancelBubble = IIf(blnCancelBubble=True,False,True)
End Sub

' ----------------------------------------
' 
' Change handler for the input box.
' 
' ----------------------------------------

Sub txtFile_OnChange()
  ' Enable the Write button.
  btnWrite.Disabled = False
  ' Enable event bubbling.
  blnCancelBubble = False
End Sub

' ----------------------------------------
' 
' Click handler for the Close button.
' 
' ----------------------------------------

Sub btnClose_OnClick()
  ' Test if we need to cancel bubbling of events.
  If blnCancelBubble = False Then
    ' Prompt the user to exit.
    If MsgBox("Are you sure you wish to exit?", _
      vbYesNo+vbDefaultButton2+vbQuestion+vbSystemModal, _
      TextToSpeech.applicationName)=vbYes Then
      ' Enable event bubbling.
      blnCancelBubble = True
      ' Close the application.
      Window.close
    End If
  End If
  ' Specify whether to bubble events.
     blnCancelBubble = IIf(blnCancelBubble=True,False,True)
End Sub

' ----------------------------------------
' 
' This is an ultra-lame workaround for the lack
' of a DoEvents() feature in HTA applications.
' 
' ----------------------------------------

Sub DoEvents()
  On Error Resume Next
  ' Create a shell object.
  Dim objShell : Set objShell = CreateObject("Wscript.Shell")
  ' Call out to the shell and essentially do nothing.
  objShell.Run "ver", 0, True
End Sub

' ----------------------------------------
' 
' This is an ultra-lame workaround for the lack
' of an IIf() function in vbscript applications.
' 
' ----------------------------------------

Function IIf(tx,ty,tz)
  If (tx) Then IIf = ty Else IIf = tz
End Function

' ----------------------------------------
' 
' Main text-to-speech function
' 
' ----------------------------------------

Sub WriteFile(strInputFileName)
  On Error Resume Next
  Dim objFSO
  Dim objFile
  Dim objSAPI
  Dim objFileStream
  Dim strOldTitle
  Dim strOutputFilename
  Const strProcessing = "Creating WAV file... "
  
  ' Define the audio format as 44.1kHz / 16-bit audio.
  ' See http://msdn.microsoft.com/en-us/library/ms720595.aspx
  Const SAFT44kHz16BitStereo = 35
  ' Allow text to be read as well as written.
  ' See http://msdn.microsoft.com/en-us/library/ms720858.aspx
  Const SSFMCreateForWrite = 3
  
  ' Define the output WAV filename.
  strOutputFilename = strInputFileName & ".wav"

  ' Create a file system object and open the input file.
  Set objFSO = CreateObject("Scripting.FileSystemObject")
  Set objFile = objFSO.OpenTextFile(strInputFileName, 1)
  
  ' Disable the form fields.
  optVoices.Disabled = True
  optRate.Disabled = True
  optVolume.Disabled = True
  txtFile.Disabled = True
  btnWrite.Disabled = True
  btnClose.Value = "Cancel"

  ' Test for an error.
  If Err.Number <> 0 Then
      MsgBox "Error: " & Err.Number & vbCrLf & Err.Description
  Else
    ' Store the original dialog title.
    strOldTitle = Document.title
    ' Display a status message.
    Document.title = strProcessing & Time()
    ' Pause briefly to let the screen refresh and capture events.
    Call DoEvents()
    ' Create a text-to-speech object.
    ' See http://msdn.microsoft.com/en-us/library/ms720149.aspx
    Set objSAPI = CreateObject("SAPI.SpVoice")
    ' Create a SAPI file stream object.
    ' See http://msdn.microsoft.com/en-us/library/ms722561.aspx
    Set objFileStream = CreateObject("SAPI.SpFileStream")
    ' Specify the stream format.
    ' See http://msdn.microsoft.com/en-us/library/ms720998.aspx
    objFileStream.Format.Type = SAFT44kHz16BitStereo
    ' Open the output file stream.
    objFileStream.Open strOutputFilename, SSFMCreateForWrite
    ' Specify the output file stream.
    ' See http://msdn.microsoft.com/en-us/library/ms723597.aspx
    Set objSAPI.AudioOutputStream = objFileStream
    
    ' Specify the speaking rate.
    ' See http://msdn.microsoft.com/en-us/library/ms723606.aspx
    objSAPI.Rate = optRate.Options(optRate.SelectedIndex).Value
    ' Specify the speaking volume.
    ' See http://msdn.microsoft.com/en-us/library/ms723615.aspx
    objSAPI.Volume = optVolume.Options(optVolume.SelectedIndex).Value
    ' Specify the voice to use.
    ' See http://msdn.microsoft.com/en-us/library/ms723601.aspx
    ' See http://msdn.microsoft.com/en-us/library/ms723614.aspx
    Set objSAPI.Voice = objSAPI.GetVoices("","").Item(optVoices.SelectedIndex)
    
    ' Loop through the lines in the input file.
    Do While Not objFile.AtEndOfStream
      ' Test if we need to cancel bubbling of events.
      If blnCancelBubble = True Then
        Exit Do
      Else
        ' Display a status message.
        Document.title = strProcessing & Time()
        ' Pause briefly to let the screen refresh and capture events.
        Call DoEvents()
        ' Speak one line from the input file.
        ' See http://msdn.microsoft.com/en-us/library/ms723609.aspx
        objSAPI.Speak objFile.ReadLine
      End If
    Loop
    ' Close the output file stream.
    objFileStream.Close
  End If

  ' Close the input file.
  objFile.Close

  ' Destroy all objects.
  Set objFileStream = Nothing
  Set objSAPI = Nothing
  Set objFile = Nothing
  Set objFSO = Nothing
  
  ' Reset the original dialog title.
  Document.title = strOldTitle

  ' Notify the user that the file has been written.
  MsgBox "Finished!", vbInformation, strOldTitle
  
  ' Re-enable the form fields.
  btnClose.Value = "Close"
  optVoices.Disabled = False
  optRate.Disabled = False
  optVolume.Disabled = False
  txtFile.Disabled = False
  btnWrite.Disabled = False

End Sub

</script>

<body bgcolor="white" id="HtmlBody">
<div id="FormControls">
  <table>
    <tr>
      <td align="left">
        <input type="file"
        style="width:250px;height:22px"
        name="txtFile"
        id="txtFile"
        onchange="txtFile_OnChange">
      </td>
      <td align="left">
        <input type="button"
        style="width:125px;height:22px"
        name="btnWrite"
        id="btnWrite"
        value="Write File"
        disabled
        onclick="btnWrite_OnClick">
      </td>
      <td align="right">
        <input type="button"
        style="width:125px;height:22px"
        name="btnClose"
        id="btnClose"
        value="Close"
        onclick="btnClose_OnClick">
      </td>
    </tr>
    <tr>
      <td align="left">
        <select name="optVoices"
          style="width:250px;height:22px">
        </select>
      </td>
      <td align="left">
        <select name="optRate"
          style="width:125px;height:22px">
          <option value="-2">Slowest</option>
          <option value="-1">Slower</option>
          <option value="0" selected>Normal Speed</option>
          <option value="1">Faster</option>
          <option value="2">Fastest</option>
        </select>
      </td>
      <td align="right">
        <select name="optVolume"
          style="width:125px;height:22px">
          <option value="25">25% Volume</option>
          <option value="50">50% Volume</option>
          <option value="75">75% Volume</option>
          <option value="100" selected>Full Volume</option>
        </select>
      </td>
    </tr>
  </table>
</div>
</body>
</html>

Note that I intentionally chose to have this HTML Application convert the text to audio one line at a time; this slows the down the conversion process, but it allows the conversion to be cancelled if necessary. (My original version of this script would convert an entire text file at one time; since there was no way to cancel the operation, the script appeared to hang when converting larger text files.)

Additional Notes

Once you have created a Wave (*.WAV) file, you can optionally convert it to an MP3 file for use in an MP3 player or mobile phone. (Most devices should playback Wave files, but MP3 files are considerably smaller and more portable.) There are a variety of Wave-to-MP3 converters out there, but I prefer to use the LAME encoder, which is an open-source code project that is available on SourceForge. Once you have the LAME encoder project compiled, (or you have located and downloaded a pre-compiled version), you can use LAME.EXE from a command prompt to convert your Wave files into MP3 files.

That being said, I prefer to automate as much as possible, so I have written a batch file which converts all of the Wave files in a directory to MP3 files and renames the source Wave files with a "*.old" filename extension:

@echo off

for /f "usebackq delims=|" %%a in (`dir /b *.wav`) do (
  for %%b in (^"%%a^") do (
    if not exist "%%~db%%~pb%%~nb.mp3" (
      lame.exe -b 128 -m j "%%a" "%%~nb.mp3"
    )
    if exist "%%~db%%~pb%%~nb.mp3" (
      move "%%a" "%%a.old"
    )
  )
)

Note that the above batch file was written for text-to-speech use, and as such it defines a bit rate of 128kbps, which would be pretty low for music files. If you want to repurpose this batch file for higher bitrates, modify the value of the "-b" parameter for the LAME.EXE command.

That wraps it up for today's post.


Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/

How to create an HTML Application to configure your IIS SMTP Username and Password settings

Like many IIS administrators, I usually install the local SMTP service on my IIS servers when I am setting up a new server from scratch. When I install the SMTP service, I configure it so that it only listens on the IP address of 127.0.0.1, so it can only send emails which originate on the server itself. What's more, I configure the SMTP service to relay all emails to a downstream SMTP service which can send emails out to the Internet. By configuring these options, I can write my ASP.NET, PHP, and Classic ASP applications so that they use the local SMTP service for all email-related functionality, which acts as a sort of message server for my applications. This system works great, and I have used this particular setup since the days of IIS 4.0. (Which was released in late 1997, as you may recall.)

That being said, in the interests of security, sometime ago I started using a downstream SMTP service which requires user credentials, (that way no one could use the downstream server anonymously). As an additional security step, I use an account which requires that the credentials are changed every 30 days or so. This is always a good security practice for myriad obvious reasons, but this meant that I needed to update the SMTP username/password settings in my IIS configuration settings every 30 days.

With that in mind, many years ago I wrote a simple VBScript application which I would use to update those credentials. At first I would simply enter the credentials directly into the script, then I would run it to update IIS, and then I would strip the credentials from the script. Needless to say, this was pretty low-tech - even considering that this was 17 or 18 years ago. I eventually updated the script so that it would use VBScript Input Boxes to prompt me for the credentials, so I no longer needed to store the credentials in the script itself. (Sometime after that I rewrote the script so that it would read the existing values from the IIS settings and pre-populate the input boxes.)

Jumping ahead a couple of years, I decided to rewrite the script as an HTML Application, which offered me considerably more options from a user interface perspective. That script has been serving me faithfully for some time now, so I thought that it would make a good blog subject.

Using the HTML Application

Using the application is pretty straight-forward; when you double click the HTA file, it will present you with the following user interface:

The script will read any existing credentials from your IIS settings and use those to pre-populate the interface. If no existing credentials are found, it will pre-populate the interface with the username of the currently-logged-on user.

Clicking Update will update your IIS settings, clicking Reset will reset the values back to the last saved version, and clicking Close will obviously close the application, but only after it has checked if you have any unsaved changes.

Creating the HTML Application

To create this HTML Application, save the following HTMLA code as "Reset SMTP Password.hta" to your computer, and then double-click its icon to run the application.

<html>
<head>
<title>Reset SMTP Password</title>
<HTA:APPLICATION
  APPLICATIONNAME="Reset SMTP Password"
  ID="ResetSmtpPassword"
  VERSION="1.0"
  BORDER="dialog"
  BORDERSTYLE="static"
  INNERBORDER="no"
  CAPTION="yes"
  SYSMENU="no"
  MAXIMIZEBUTTON="no"
  MINIMIZEBUTTON="no"
  SCROLL="no"
  SCROLLFLAT="yes"
  SINGLEINSTANCE="yes"
  CONTEXTMENU="no"
  SELECTION="no"/>
<style>
<!--
body,input
{
font-family:calibri,arial;
font-size:9pt;
color: #000;
background-color: #fff;
}
table,td,th
{
border:none;
}
--> </style> </head> <script language="VBScript"> Option Explicit ' Define the global variables. Dim objWMIService, objIIsSmtpServer Dim strRouteUserName, strRoutePassword Dim blnCancelBubble, blnPendingUpdates ' ---------------------------------------- ' ' Initialization method for the application. ' ' ---------------------------------------- Sub Window_OnLoad ' Define the local variables. Dim objNetwork ' Set up the UI dimensions. Const intDialogWidth = 280 Const intDialogHeight = 220 ' Specify the window position and size. Self.resizeTo intDialogWidth,intDialogHeight Self.moveTo (Screen.AvailWidth - intDialogWidth) / 2, _ (Screen.AvailHeight - intDialogHeight) / 2 ' Enable events. blnCancelBubble = False blnPendingUpdates = False ' Set up some base objects for the local computer and default SMTP instance. ' Note that these settings can be customized for a different computer or SMTP instance. Set objWMIService = GetObject("winmgmts:{impersonationLevel=impersonate}!\\.\root\cimv2") Set objIIsSmtpServer = GetObject("IIS://localhost/SmtpSvc/1") ' Retrieve the current username/password from the SMTP settings. strRouteUserName = objIIsSmtpServer.RouteUserName strRoutePassword = objIIsSmtpServer.RoutePassword ' Verify that a username was retrieved; otherwise, use the logged-on user. If Len(strRouteUserName)=0 Then Set objNetwork = CreateObject("WScript.Network") strRouteUserName = IIf(Len(objNetwork.UserDomain)>0, _ objNetwork.UserDomain & "\","") & objNetwork.UserName Set objNetwork = Nothing blnPendingUpdates = True End If ' Store the username/password values in the UI. txtUsername.value = strRouteUserName txtPassword.value = strRoutePassword End Sub ' ---------------------------------------- ' ' Implement the missing IIf() function. ' ' ---------------------------------------- Function IIf(tx,ty,tz) If (tx) Then IIf = ty Else IIf = tz End Function ' ---------------------------------------- ' ' Click handler for the Close button. ' ' ---------------------------------------- Sub btnClose_OnClick() ' Test if we need to cancel bubbling of events. If blnCancelBubble = False Then ' Check if there are pending updates. If blnPendingUpdates = False Then ' If not, then close the application. Window.close ' Prompt the user to exit. ElseIf MsgBox("You have not saved your changes." & vbCrLf & _ "Are you sure you wish to exit?", _ vbYesNo+vbDefaultButton2+vbQuestion+vbSystemModal, _ ResetSmtpPassword.applicationName)=vbYes Then ' Enable event bubbling. blnCancelBubble = True ' Close the application. Window.close End If End If ' Specify whether to bubble events. blnCancelBubble = IIf(blnCancelBubble=True,False,True) End Sub ' ---------------------------------------- ' ' Change handler for text boxes. ' ' ---------------------------------------- Sub Textbox_OnChange() ' Flag the application as having updates pending. blnPendingUpdates = True End Sub ' ---------------------------------------- ' ' Focus handler for text boxes. ' ' ---------------------------------------- Sub Textbox_OnFocus(objTextbox) ' Select the text in the textbox. objTextbox.Select End Sub ' ---------------------------------------- ' ' Click handler for the Reset button. ' ' ---------------------------------------- Sub btnReset_OnClick() ' Reset the username/password values in the UI. txtUsername.value = strRouteUserName txtPassword.value = strRoutePassword blnPendingUpdates = False End Sub ' ---------------------------------------- ' ' Click handler for the Update button. ' ' ---------------------------------------- Sub btnUpdate_OnClick() ' Catch bubbled events. If blnCancelBubble = True Then blnCancelBubble = False Exit Sub End If ' Verify valid data. If Len(txtUsername.value)=0 Or Len(txtPassword.value)=0 Then ' Inform the user that they made a mistake. MsgBox "An invalid username or password was entered.", _ vbCritical + vbOKOnly, ResetSmtpPassword.applicationName ' Cancel event bubbling. blnCancelBubble = True Else ' Store the username/password values for the SMTP server. objIIsSmtpServer.RouteUserName = txtUsername.value objIIsSmtpServer.RoutePassword = txtPassword.value objIIsSmtpServer.SetInfo ' Save the username/password values. strRouteUserName = txtUsername.value strRoutePassword = txtPassword.value ' Flag the application as having no updates pending. blnPendingUpdates = False ' Cancel event bubbling. blnCancelBubble = True End If End Sub </script> <body bgcolor="white" id="HtmlBody"> <div id="FormControls"> <table> <tr><td>Please enter your SMTP credentials:</td></tr> <tr> <td align="left"> <input type="text" style="width:250px;height:22px" name="txtUsername" id="txtUsername" onchange="Textbox_OnChange()" onfocus="Textbox_OnFocus(txtUsername)" /> </td> </tr> <tr> <td align="left"> <input type="password" style="width:250px;height:22px" name="txtPassword" id="txtPassword" onchange="Textbox_OnChange()" onfocus="Textbox_OnFocus(txtPassword)" /> </td> </tr> <tr> <td align="right"> <input type="button" style="width:125px;height:22px" name="btnUpdate" id="btnUpdate" value="Update" onclick="btnUpdate_OnClick()" /> </td> </tr> <tr> <td align="right"> <input type="button" style="width:125px;height:22px" name="btnReset" id="btnReset" value="Reset" onclick="btnReset_OnClick()" /> </td> </tr> <tr> <td align="right"> <input type="button" style="width:125px;height:22px" name="btnClose" id="btnClose" value="Close" onclick="btnClose_OnClick()" /> </td> </tr> </table> </div> </body> </html>

That's all that there is to it, although you might want to restart your SMTP service after you have made these changes.

Additional Notes

On a related topic, I get asked from time to time why I like to use HTML Applications (HTMLA) for some of my scripting examples, and the answer is quite simple: it is very easy to create powerful scripts in a very short amount of time which have sophisticated user interfaces and no compilation requirements.

I use Adersoft's HtaEdit as my IDE for HTMLA, which allows me to do normal development tasks like configuring project options, setting breakpoints, and stepping through my code.


Note: Click the image above to open it full-size in a separate window.

That being said, I have been experimenting with creating user interfaces in PowerShell, and it looks like it has some real promise for creating great UIs, too. But for now I only use PowerShell to create command line scripts, I use HTMLA to create cool UIs for administrative scripts, and I use C# to create "real" applications.


Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/