Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / VB

Audio Book Creator

0.00/5 (No votes)
23 May 2023CPOL5 min read 3.8K   307  
This application lets you create an audio book using Eleven Labs API.
This Windows Desktop application will create an mp3/mp4 files from a text file. These files can then be uploaded to YouTube or Audible.

Introduction

This application lets you create an audio book using Eleven Labs API. Eleven Labs API allows only 5,000 characters per request. The application will generate mp3 file for each paragraph and then merge them together.

Image 1

If you have a pdf file, the text file needs to be generated by opening the pdf file in Word and copied and pasted to a text file. Open the text file in a text editor that has line numbers such as Notepad2. Open the file in and remove text that would not be read like: table of contents, footnotes, index and references. Each paragraph must be on one line. 

There is a 0.3 second pause between paragraphs. One blank line means 1 second pause. Two blank lines means new Chapter.

One blank line means 1 second pause. But you can use the “Silence” feature to customize the pause duration.

How to Create an Audiobook

  • First get API Key from Eleven Labs (https://beta.elevenlabs.io/pricing)
  • Select Eleven Labs “Voice”. A custom voice can be created and used.
  • “Say it” will generate mp3 file based on any text and place the file into Temp folder in the same folder and the EXE.
  • “Highlight text when” option helps you with text file correction and editing before to generate the mp3 files. For example, “Begins with lower case character” and “Begins with a number” option will highlight paragraphs that might be broken during Word PDF conversion. “Contains number” might help identify paragraphs that contain a footnote number.
  • “Save text file” saved the changes in the text file. “Backup text file” option creates a backup to let you undo the changes you made to the text file. The backup text files will be placed in the folder with the same name as the text file name plus “_backup”.
  • 1. Process Text File” will generate MP3 file for each line in the text file. This might cost you about $50 depending on the size of the file. (The current price is $99 per 0.5 MB. An average book is about 0.25 MB.) The file will be placed in the folder with the same name as the text file name. Each file will be named after the line number in the text file like 0001.mp3. This means that you should not add or delete lines to the text file after MP3 files are generated.
  • Select a line in the grid and click “Play” to play an MP3 file. Click to “Stop” to stop the mp3 file playing. Select a line in the grid and click “Delete” to delete an MP3 file.
  • You can delete bad mp3 files and click “1. Process Text File” again to regenerate the mp3 files that were deleted.
  • You can also select a line in the grid and click “Regenerate” to re-create the MP3 file. This option will also save the text file if needed.
  • Play on key up” option allows you to listen to the entire book by pressing the arrow down key after selecting a line text.
  • Once you are satisfied with the quality of the generated mp3 files, click “2. MP3 Chapters” to generate mp3 file for each chapter. Two blank lines in the text file means new Chapter. The files will be placed in the folder with the same name as the text file name plus “-Chapters”.
  • Merge MP3 Chapter files into one mp3 file. The mp3 file will be placed in the folder as the text file and have the same name but with mp3 extension.

Uploading Audiobook to YouTube

  • First select Image file to be used for mp4 file generation.
  • 3. Make MP4 Files” will generate mp4 file for each chapter. This operation uses mp3 files from “-Chapters” folder. This operation can take about 8 hours. These files can then be uploaded to YouTube. The files will be placed in the folder with the same name as the text file name plus -Videos
  • Make Video File” will generate one mp4 file. These files can then be uploaded to YouTube. The file will be generated by merging MP4 chapter files if they are available. MP4 chapter files are not available. The single mp3 file will be used, but the operation can take about 8 hours. The MP4 file will be placed in the folder as the text file and have the same name but with mp4 extension.
  • Generate YouTube index from Chapters mp3 file duration. The index can be used in the Video description or the comment section. Note that MP4 files night be generated with a different duration so the Index might need to be adjusted.

Here is an audiobook I created using this app.

Uploading Audiobook to Audible (acx.com)

  • Audible requires MP3 file with 192 bitrate. “Make 192 Bitrate files” operation uses mp3 files from “-Chapters” folder. The mp3 192 bitrate files will be placed in the folder with the same name as the text file name plus “-Chapters192”.
  • Audible (acx.com) sometimes requires the mp3 file values to be increased or decreased. “Change Volume” option allows you to do that. If ACX still complains, try using Audacity.

Using the Code

Code is using ffmpeg.exe to convert mp3 to mp4 and change mp3 bitrate. Here is the VB.NET code for the main form:

VB.NET
Imports System.Net
Imports System.IO

Public Class Form1

  'Dim API_KEY As String = "" 'https://beta.elevenlabs.io - Profile 
  'Dim sVoiceId As String = "21m00Tcm4TlvDq8ikWAM" 'Rachel
  Dim sVoiceId As String = "ErXwobaYiN019PkySvjV" 'Antoni
  Dim oVoices As New Hashtable
  Dim oAppSetting As New AppSetting()
  Dim bStop As Boolean = False

  Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
    selSilence.SelectedIndex = 0

    oAppSetting.LoadData()
    txtImageFile.Text = oAppSetting.GetValue("ImageFile")
    txtSrcFile.Text = oAppSetting.GetValue("SrcFile")
    txtText.Text = oAppSetting.GetValue("Text")
    txtApiKey.Text = oAppSetting.GetValue("ApiKey")

    If txtApiKey.Text <> "" Then
      txtApiKey.PasswordChar = "*"
    End If

    If IO.File.Exists(txtSrcFile.Text) = False Then
      txtSrcFile.Text = ""
    End If

    If IO.File.Exists(txtImageFile.Text) = False Then
      txtImageFile.Text = ""
    End If

    Dim sVoice As String = oAppSetting.GetValue("Voice", "Antoni")
    If sVoice <> "Antoni" And txtApiKey.Text <> "" Then

      Try
        SetVoices()
      Catch ex As Exception
        txtApiKey.Text = ""
      End Try

      SetVoiceSelect(sVoice)
    Else
      cbVoice.SelectedIndex = 0
    End If

    UpdateFileGrid()

    Dim sSelectedRowIndex As String = oAppSetting.GetValue("SelectedRowIndex")
    If sSelectedRowIndex <> "" Then
      Dim iRowIndex As Integer = sSelectedRowIndex
      If iRowIndex <> -1 AndAlso iRowIndex < DataGridView1.RowCount Then
        DataGridView1.MultiSelect = False
        DataGridView1.Rows(iRowIndex).Cells(0).Selected = True
        SetupLineText()
      End If
    End If

    Dim sTootip As String = ""

    ToolTip1.AutoPopDelay = 32767
    ToolTip1.SetToolTip(btnProcessTextFile, "Generate MP3 file for _
                           each line in the text file. " & vbCrLf & _
              "This might cost you about $50 depending _
                            on the size of the file." & vbCrLf & _
              "The file will be placed in the folder with _
                            the same name as the text file name. " & vbCrLf & _
              "Each file will be named after the line number _
                            in the text file like 0001.mp3. " & vbCrLf & _
              "This means that you should not add or deleted lines _
                            to the text file after MP3 files are generated.")

    ToolTip1.SetToolTip(btnChapters, "Generate MP3 file for each chapter. _
                Two blank lines in the text file means new Chapter." & vbCrLf & _
        "The files will be placed in the folder with the same name _
                 as the text file name plus -Chapters")

    ToolTip1.SetToolTip(btnMakeVideos, "Generate MP4 file for each chapter. _
                This operation uses mp3 files from -Chapters folder." & vbCrLf & _
        "This operation can take about 8 hours. These files can then be _
                 uploaded to YouTube." & vbCrLf & _
        "The files will be placed in the folder with the same name _
                 as the text file name plus -Videos")

    ToolTip1.SetToolTip(btnMerge, "Merge MP3 Chapter files into one MP3 file." & _
                vbCrLf & "The MP3 file will be placed in the folder as the text file _
                and have the same name but with MP3 extension.")

    ToolTip1.SetToolTip(btnMakeVideo, "Generate one MP4 file. _
                These file can then be uploaded to YouTube. " & vbCrLf & _
        "The file will be generated by merging MP4 chapter files _
                 if they are available. " & vbCrLf & _
        "If MP4 chapter files are not available the single MP3 file _
                 will be used but the operation can take about 8 hours." & vbCrLf & _
        "The MP4 file will be placed in the folder as the text file _
                 and have the same name but with MP4 extension.")

    ToolTip1.SetToolTip(btnYouTubeIndex, "Generate YouTube index _
                from Chapters MP3 file duration. " & vbCrLf & _
        "The index can be used in the Video description _ 
                 or the comment section. " & vbCrLf & _
         "Note that MP4 files night be generated with a different duration _
                 so the Index might need to be adjusted.")

    ToolTip1.SetToolTip(btn192Bitrate, "Audible requires MP3 file _
            with 192 bitrate. This operation uses mp3 files from _
            -Chapters folder." & vbCrLf & "The mp3 192 bitrate files will be _
       placed in the folder with the same name as the text file name _
            plus -Chapters192")

    ToolTip1.SetToolTip(btnChangeVolume, "Audible (acx.com) _
           sometimes requires the mp3 file values to be increased or _
           decreased. " & vbCrLf & "This option allows you to do that. _
          If ACX still complains try using Audacity.")

    ToolTip1.SetToolTip(btnPlay, "Select a line in the grid and _
                           click play to MP3 file.") 
    ToolTip1.SetToolTip(btnStopPlay, "Click to stop the MP3 file playing")
    ToolTip1.SetToolTip(btnDelete, "Select a line in the grid and _
                           click delete to MP3 file.")

    ToolTip1.SetToolTip(btnReGenerate, "Select a line in the grid _
                           and click Generate (to save the text file if needed)  _
                           and re-create the MP3 file.")
    ToolTip1.SetToolTip(btnSave, "Save the changes in the text file")
    ToolTip1.SetToolTip(chkPlayOnKeyUp, "This option allows you to listen _
                to the entire book by pressing the arrow down key _
                after selecting a line text.")
    ToolTip1.SetToolTip(selHighlight, "This option helps you with _
       text file correction and editing before to generating the mp3 files.")

    sTootip = "For testing generate mp3 file based on any text"
    ToolTip1.SetToolTip(txtText, sTootip)
    ToolTip1.SetToolTip(btnSayIt, sTootip)

    sTootip = "API Key from Eleven Labs (https://beta.elevenlabs.io/pricing)"
    ToolTip1.SetToolTip(txtApiKey, sTootip)
    ToolTip1.SetToolTip(btnApiKeyShow, sTootip)

    sTootip = "If you have pdf file, the text file can be generated _
                  by opening the pdf file in Word and copy and pasting to _
                  a text file." & vbCrLf &
         " Open the text file in a text editor that has line numbers _
                   (such as Notepad2). "
    ToolTip1.SetToolTip(txtSrcFile, sTootip)
    ToolTip1.SetToolTip(btnSrcFile, sTootip)

    sTootip = "Image file to be used for mp4 file generation."
    ToolTip1.SetToolTip(txtImageFile, sTootip)
    ToolTip1.SetToolTip(btnImageFile, sTootip)

    sTootip = "Eleven Labs Voice. A custom voice can be created."
    ToolTip1.SetToolTip(cbVoice, sTootip)
    ToolTip1.SetToolTip(btnReloadVoices, sTootip)

    sTootip = "One blank line means 1 second pause. 
                  Use this in case you need to customize the pause duration."
    ToolTip1.SetToolTip(selSilence, sTootip)
    ToolTip1.SetToolTip(btnSilence, sTootip)

    ToolTip1.SetToolTip(chkBackupFile, "Use this option if you want to _
                           undo the changes you made to the text file. " & _
      "The backup text files will be placed in the folder with the _
            same name as the text file name plus _backup")

    ToolTip1.SetToolTip(urlApiKey, "Profile > API Key")

    ToolTip1.SetToolTip(chkImageText, _
                 "Add Chapter file name to the video file image")

  End Sub

  Private Sub Form1_FormClosing(sender As Object, _
   e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing
    oAppSetting.SetValue("Voice", cbVoice.Text)
    oAppSetting.SetValue("SrcFile", txtSrcFile.Text)
    oAppSetting.SetValue("ImageFile", txtImageFile.Text)
    oAppSetting.SetValue("Text", txtText.Text)
    oAppSetting.SetValue("ApiKey", txtApiKey.Text)
    oAppSetting.SetValue("SelectedRowIndex", GetSelectedRowIndex())
    oAppSetting.SaveData()
  End Sub

  Private Sub btnStop_Click(sender As Object, e As EventArgs) Handles btnStop.Click
    bStop = True
  End Sub

  Private Sub btnProcessTextFile_Click(sender As Object, e As EventArgs) _
               Handles btnProcessTextFile.Click

    If MsgBox("Are you sure you want to process the text file? _
                  This might cost you about $50.", vbYesNo) <> vbYes Then
      Exit Sub
    End If

    btnProcessTextFile.Enabled = False
    My.Application.DoEvents()

    ProcessTextFile(0)
    UpdateFileGrid()

    btnProcessTextFile.Enabled = True
    MsgBox("Done")
  End Sub

  Sub ProcessTextFile(ByVal iProcessRow As Integer)

    If txtApiKey.Text = "" Then
      MsgBox("API Key is missing")
      Exit Sub
    End If

    Dim sFilePath As String = txtSrcFile.Text
    If sFilePath = "" OrElse IO.File.Exists(sFilePath) = False Then
      txtSrcFile.Text = ""
      MsgBox("Text file is blank")
      Exit Sub
    End If

    Dim sMp3FolderePath As String = GetFolderPath("mp3")
    If sMp3FolderePath = "" Then
      MsgBox("Could not find mp3 folder")
      Exit Sub
    End If

    Dim sBlankFilePath As String = sMp3FolderePath & "\1sec.mp3"
    If IO.File.Exists(sBlankFilePath) = False Then
      MsgBox("Could not find " & sBlankFilePath)
      Exit Sub
    End If

    Dim sVoice As String = cbVoice.Text
    If oVoices.ContainsKey(sVoice) Then
      sVoiceId = oVoices(sVoice)
    End If

    If iProcessRow = 0 Then
      lbCount.Visible = True
      btnStop.Visible = True
      ProgressBar1.Visible = True
    End If

    Dim sFolderPath As String = Path.GetDirectoryName(sFilePath)
    Dim sFileName As String = Path.GetFileNameWithoutExtension(sFilePath)
    Dim sDestFolderPath As String = Path.Combine(sFolderPath, sFileName)

    If Not System.IO.Directory.Exists(sDestFolderPath) Then
      System.IO.Directory.CreateDirectory(sDestFolderPath)
    End If

    Dim iRows As Integer = GetFileRowsCount(sFilePath)
    If iRows = 0 Then
      Exit Sub
    End If

    Dim iMaxSize As Integer = iRows.ToString().Length

    If iProcessRow = 0 Then
      ProgressBar1.Maximum = iRows
    End If

    Dim iRow As Integer = 0
    Dim oStreamReader As System.IO.StreamReader = GetStreamReader(sFilePath)
    Dim sLine As String = oStreamReader.ReadLine()
    Do Until sLine Is Nothing
      iRow += 1

      If iProcessRow = 0 OrElse iRow = iProcessRow Then

        If iProcessRow = 0 Then
          lbCount.Text = iRow & "/" & iRows
        End If

        Dim sDestFileBase As String = _
               Microsoft.VisualBasic.Right("000000" & iRow, iMaxSize)
        Dim sDestFileName As String = sDestFileBase & ".mp3"
        Dim sDestFilePath As String = _
                                  Path.Combine(sDestFolderPath, sDestFileName)

        If IO.File.Exists(sDestFilePath) = False Then
          If Trim(sLine) = "{{100ms.mp3}}" Then
            IO.File.Copy(sMp3FolderePath & "\100ms.mp3", sDestFilePath)

          ElseIf Trim(sLine) = "{{200ms.mp3}}" Then
            IO.File.Copy(sMp3FolderePath & "\200ms.mp3", sDestFilePath)

          ElseIf Trim(sLine) = "{{300ms.mp3}}" Then
            IO.File.Copy(sMp3FolderePath & "\300ms.mp3", sDestFilePath)

          ElseIf Trim(sLine) = "{{400ms.mp3}}" Then
            IO.File.Copy(sMp3FolderePath & "\400ms.mp3", sDestFilePath)

          ElseIf Trim(sLine) = "{{500ms.mp3}}" Then
            IO.File.Copy(sMp3FolderePath & "\500ms.mp3", sDestFilePath)

          ElseIf Trim(sLine) = "{{600ms.mp3}}" Then
            IO.File.Copy(sMp3FolderePath & "\600ms.mp3", sDestFilePath)

          ElseIf Trim(sLine) = "{{700ms.mp3}}" Then
            IO.File.Copy(sMp3FolderePath & "\700ms.mp3", sDestFilePath)

          ElseIf Trim(sLine) = "{{800ms.mp3}}" Then
            IO.File.Copy(sMp3FolderePath & "\800ms.mp3", sDestFilePath)

          ElseIf Trim(sLine) = "{{900ms.mp3}}" Then
            IO.File.Copy(sMp3FolderePath & "\900ms.mp3", sDestFilePath)

          ElseIf Trim(sLine) = "" Or Trim(sLine) = "{{1sec.mp3}}" Then
            'Copy Blank mp3 file
            IO.File.Copy(sBlankFilePath, sDestFilePath)
          Else
            If TextToSpeach(sLine, sDestFilePath) Then
              'Success
            Else
              MsgBox("Could not generate file for line: " _
                                   & iRow & ", Text: " & sLine)
            End If
          End If
        End If

      End If 'iProcessRow = 0 OrElse iRow = iProcessRow

      sLine = oStreamReader.ReadLine()

      If iProcessRow = 0 Then
        ProgressBar1.Value = iRow

        My.Application.DoEvents()
        If bStop Then
          bStop = False
          MsgBox("Stopped Processing at row " & iRow & ". _
                           There are " & iRows & " rows.")
          Exit Do
        End If
      End If

    Loop

    oStreamReader.Close()

    If iProcessRow = 0 Then
      lbCount.Visible = False
      btnStop.Visible = False
      ProgressBar1.Value = 1
      ProgressBar1.Visible = False
    End If

  End Sub

  Private Function GetFileRowsCount(ByVal sFilePath As String) As Integer
    Dim oStreamReader As System.IO.StreamReader = GetStreamReader(sFilePath)
    Dim sLine As String = oStreamReader.ReadLine()
    Dim iRow As Integer = 0

    Do Until sLine Is Nothing
      iRow += 1
      sLine = oStreamReader.ReadLine()
    Loop

    oStreamReader.Close()

    Return iRow
  End Function

  Private Function GetAssFolderPath() As String
    Dim sAssPath As String = _
           System.Reflection.Assembly.GetExecutingAssembly().Location
    Return System.IO.Path.GetDirectoryName(sAssPath)
  End Function

  Private Function GetFfmpegFile() As String
    'https://www.gyan.dev/ffmpeg/builds/
    Dim sFolderPath As String = GetAssFolderPath()
    Dim sExePath As String = IO.Path.Combine(sFolderPath, "ffmpeg.exe")
    If IO.File.Exists(sExePath) Then
      Return sExePath
    End If

    Dim sFfmpegFolder As String = GetFolderPath("ffmpeg")
    Return sFfmpegFolder & "\bin\ffmpeg.exe"
  End Function

  Private Function GetFolderPath(ByVal sFolderName As String) As String

    Dim sPath As String = GetAssFolderPath()

    For i As Integer = 0 To 3
      Dim sRetPath As String = IO.Path.Combine(sPath, sFolderName)

      If IO.Directory.Exists(sRetPath) Then
        Return sRetPath
      End If

      Try
        sPath = IO.Directory.GetParent(sPath).FullName
      Catch ex As Exception
        Return ""
        'MsgBox("GetFolderPath(), Could not get parent of: " & sPath)
      End Try

    Next

    Return ""
  End Function

  Private Function GetTempFolder() As String
    Dim sTempFolder As String = GetFolderPath("Temp")
    If IO.Directory.Exists(sTempFolder) = False Then
      sTempFolder = IO.Path.Combine(GetAssFolderPath(), "Temp")

      If IO.Directory.Exists(sTempFolder) = False Then
        IO.Directory.CreateDirectory(sTempFolder)
      End If
    End If

    Return sTempFolder
  End Function

  Private Sub btnSayIt_Click(sender As Object, e As EventArgs) Handles btnSayIt.Click

    If txtApiKey.Text = "" Then
      MsgBox("API Key is missing")
      Exit Sub
    End If

    Dim sTempFolder As String = GetTempFolder()
    Dim sFilePath As String = IO.Path.Combine(sTempFolder, GetGuidFileName("mp3"))

    Dim sVoice As String = cbVoice.Text
    If oVoices.ContainsKey(sVoice) Then
      sVoiceId = oVoices(sVoice)
    End If

    If TextToSpeach(txtText.Text, sFilePath) Then
      If IO.File.Exists(sFilePath) Then

        Dim sDestFilePath As String = _
               IO.Path.Combine(sTempFolder, PadFileName(txtText.Text) & ".mp3")

        Try
          If IO.File.Exists(sDestFilePath) Then
            PlaySoundStop()

            IO.File.Delete(sDestFilePath)
          End If

          IO.File.Move(sFilePath, sDestFilePath)
          sFilePath = sDestFilePath
        Catch ex As Exception
          'Ignore
        End Try

        txtTestFile.Visible = True
        txtTestFile.Text = sFilePath

        PlaySound(sFilePath)
      End If
    End If

  End Sub

  Private Function TextToSpeach(sText As String, sFilePath As String) As Boolean

    If Trim(sText) = "" Then
      Return False
    End If

    If txtApiKey.Text = "" Then
      MsgBox("Set API Key")
      Return False
    End If

    System.Net.ServicePointManager.SecurityProtocol =
      System.Net.SecurityProtocolType.Ssl3 Or
      System.Net.SecurityProtocolType.Tls12 Or
      System.Net.SecurityProtocolType.Tls11 Or
      System.Net.SecurityProtocolType.Tls

    'https://docs.elevenlabs.io/api-reference/text-to-speech
    Dim apiEndpoint As String = _
       "https://api.elevenlabs.io/v1/text-to-speech/" & sVoiceId 'Rachel
    Dim request As HttpWebRequest = WebRequest.Create(apiEndpoint)
    request.Method = "POST"
    request.ContentType = "application/json"
    request.Accept = "audio/mpeg"
    request.Headers.Add("xi-api-key", txtApiKey.Text)

    Dim data As String = "{"
    data += " ""text"":""" & PadQuotes(sText) & ""","
    'data += " ""voice_settings"": {""stability"": 0,""similarity_boost"": 0}"
    data += " ""voice_settings"": {""stability"": 0.5,""similarity_boost"": 1}"
    data += "}"

    Using streamWriter As New StreamWriter(request.GetRequestStream())
      streamWriter.Write(data)
      streamWriter.Flush()
      streamWriter.Close()
    End Using

    Dim response As HttpWebResponse = request.GetResponse()

    If response.StatusCode = 200 Then
      Dim oFileStream As FileStream = IO.File.Create(sFilePath)
      response.GetResponseStream().CopyTo(oFileStream)
      oFileStream.Close()
      Return True
    Else
      Return False
    End If

  End Function

  Private Sub btnReloadVoices_Click(sender As Object, e As EventArgs) _
                                     Handles btnReloadVoices.Click
    SetVoices()
  End Sub

  Private Sub SetVoices()

    If txtApiKey.Text = "" Then
      MsgBox("Set API Key")
      Exit Sub
    End If

    System.Net.ServicePointManager.SecurityProtocol =
     System.Net.SecurityProtocolType.Ssl3 Or
     System.Net.SecurityProtocolType.Tls12 Or
     System.Net.SecurityProtocolType.Tls11 Or
     System.Net.SecurityProtocolType.Tls

    Dim apiEndpoint As String = "https://api.elevenlabs.io/v1/voices"
    Dim request As HttpWebRequest = WebRequest.Create(apiEndpoint)
    request.Method = "GET"
    request.ContentType = "application/json"
    request.Headers.Add("xi-api-key", txtApiKey.Text)

    Dim response As HttpWebResponse = request.GetResponse()
    Dim streamReader As New StreamReader(response.GetResponseStream())
    Dim sJson As String = streamReader.ReadToEnd()

    Dim sVoice As String = cbVoice.Text
    cbVoice.Items.Clear()

    'Dim sIds As String = ""
    Dim oSortedList As SortedList = New SortedList()
    Dim oJavaScriptSerializer As _
           New System.Web.Script.Serialization.JavaScriptSerializer
    Dim oJson As Hashtable = oJavaScriptSerializer.Deserialize(Of Hashtable)(sJson)
    Dim oList As Object() = oJson("voices")
    For i As Integer = 0 To oList.Length - 1
      Dim sId As String = oList(i)("voice_id")
      Dim sName As String = oList(i)("name")
      oSortedList.Add(sName, sName)
      oVoices(sName) = sId
    Next

    For Each oItem As DictionaryEntry In oSortedList
      cbVoice.Items.Add(oItem.Key)
    Next

    SetVoiceSelect(sVoice)

  End Sub

  Private Sub SetVoiceSelect(sVoice As String)
    For i As Integer = 0 To cbVoice.Items.Count - 1
      If cbVoice.Items(i) = sVoice Then
        cbVoice.SelectedIndex = i
        Exit For
      End If
    Next
  End Sub

  Private Function PadQuotes(ByVal s As String) As String

    If s.IndexOf("\") <> -1 Then
      s = Replace(s, "\", "\\")
    End If

    If s.IndexOf(vbCrLf) <> -1 Then
      s = Replace(s, vbCrLf, "\n")
    End If

    If s.IndexOf(vbCr) <> -1 Then
      s = Replace(s, vbCr, "\r")
    End If

    If s.IndexOf(vbLf) <> -1 Then
      s = Replace(s, vbLf, "\f")
    End If

    If s.IndexOf(vbTab) <> -1 Then
      s = Replace(s, vbTab, "\t")
    End If

    If s.IndexOf("""") = -1 Then
      Return s
    Else
      Return Replace(s, """", "\""")
    End If
  End Function

  Dim oPlayer As Object = Nothing

  Sub PlaySound()
    Dim sFilePath As String = GetSelectedFielPath()
    If sFilePath <> "" Then
      PlaySound(sFilePath)
    Else
      MsgBox("MP3 file does not exist " & sFilePath)
    End If
  End Sub

  Sub PlaySound(sSoundFile As String)

    If IO.File.Exists(sSoundFile) = False Then
      Exit Sub
    End If

    PlaySoundStop()
    btnStopPlay.Enabled = True

    oPlayer = CreateObject("WMPlayer.OCX")
    oPlayer.URL = sSoundFile
    oPlayer.controls.play()
  End Sub

  Sub PlaySoundStop()
    If oPlayer IsNot Nothing Then
      oPlayer.controls.stop()
      oPlayer.Close()
      oPlayer = Nothing
      btnStopPlay.Enabled = False
    End If
  End Sub

  Private Sub btnStopPlay_Click(sender As Object, e As EventArgs) _
               Handles btnStopPlay.Click
    PlaySoundStop()
  End Sub

  Public Function GetGuidFileName(ByVal sExt As String) As String
    Return System.Guid.NewGuid().ToString("N") + "." + sExt
  End Function

  Private Sub btnSrcFile_Click(sender As Object, e As EventArgs) _
               Handles btnSrcFile.Click
    OpenFileDialog1.FileName = txtSrcFile.Text
    OpenFileDialog1.Title = "Open Text File"
    OpenFileDialog1.Filter = "TXT files|*.txt"
    OpenFileDialog1.ShowDialog()

    If OpenFileDialog1.FileName <> "" Then
      txtSrcFile.Text = OpenFileDialog1.FileName
    End If

    UpdateFileGrid()
  End Sub

  Private Sub btnImageFile_Click(sender As Object, e As EventArgs) _
               Handles btnImageFile.Click

    OpenFileDialog1.FileName = txtSrcFile.Text
    OpenFileDialog1.Title = "Image File"
    OpenFileDialog1.Filter = "Image files|*.jpg;*.png"
    OpenFileDialog1.ShowDialog()

    If OpenFileDialog1.FileName <> "" Then
      txtImageFile.Text = OpenFileDialog1.FileName
    End If

  End Sub

  Private Sub UpdateFileGrid()
    Dim sFilePath As String = txtSrcFile.Text
    If sFilePath = "" OrElse File.Exists(sFilePath) = False Then
      txtSrcFile.Text = ""
      DataGridView1.DataSource = Nothing
      DataGridView1.Update()
      Exit Sub
    End If

    Dim sFolderPath As String = Path.GetDirectoryName(sFilePath)
    Dim sFileName As String = Path.GetFileNameWithoutExtension(sFilePath)
    Dim sDestFolderPath As String = Path.Combine(sFolderPath, sFileName)
    Dim iRowIndex As Integer = GetSelectedRowIndex()

    Dim oTable As Data.DataTable = _
           GetDataTableFromFolder(sFilePath, sDestFolderPath)
    DataGridView1.DataSource = oTable
    DataGridView1.Update()

    DataGridView1.Columns("Size").Visible = False
    DataGridView1.Columns("FilePath").Visible = False
    'DataGridView1.Columns("Length").DefaultCellStyle.Format = "#,#"

    DataGridColor()
    DataGridResize()

    If iRowIndex <> -1 And iRowIndex < DataGridView1.RowCount Then
      DataGridView1.MultiSelect = False
      DataGridView1.Rows(iRowIndex).Cells(0).Selected = True
    End If

  End Sub

  Private Sub DataGridColor()

    Dim sHighlight As String = selHighlight.Text

    For iRow = 0 To DataGridView1.RowCount - 1
      Dim oRow As DataGridViewRow = DataGridView1.Rows(iRow)
      If oRow.IsNewRow = False Then
        Dim iSize As Integer = oRow.Cells("Size").Value

        If iSize = 5856 Then
          For Each oCell As DataGridViewCell In oRow.Cells
            oCell.Style.BackColor = Color.LightBlue
          Next
        ElseIf iSize = 0 Then
          For Each oCell As DataGridViewCell In oRow.Cells
            oCell.Style.BackColor = Color.LightCoral
          Next
        Else
          For Each oCell As DataGridViewCell In oRow.Cells
            oCell.Style.BackColor = Color.White
          Next
        End If

        Dim sText As String = oRow.Cells("Text").Value & ""

        If Len(sText) > 5000 Then
          'https://beta.elevenlabs.io/faq
            'https://help.elevenlabs.io/hc/en-us/articles/13298164480913-What-s-the-maximum-amount-of-text-I-can-generate-
          oRow.Cells("Size").Style.BackColor = Color.Red
        End If

        If sHighlight <> "" Then

          Dim sFirstChar As String = Microsoft.VisualBasic.Left(sText, 1)

          Select Case sHighlight
            Case "Begins with number"
              If IsNumeric(sFirstChar) Then
                oRow.Cells("Text").Style.BackColor = Color.Yellow
              End If

            Case "Contains number"
              If System.Text.RegularExpressions.Regex.IsMatch_
                              (sText, "\b\w+\s*\d+\b") Then
                oRow.Cells("Text").Style.BackColor = Color.Yellow
              End If

            Case "Begins with lower case character"
              If sFirstChar <> UCase(sFirstChar) Then
                oRow.Cells("Text").Style.BackColor = Color.Yellow
              End If
          End Select
        End If

      End If
    Next
  End Sub

  Private Sub DataGridResize()
    If DataGridView1.Columns.Count > 3 Then
      Dim w As Integer = DataGridView1.Width - 85
      w = w - DataGridView1.Columns("Name").Width
      w = w - DataGridView1.Columns("Length").Width
      DataGridView1.Columns("Text").Width = Math.Max(w, 200)
    End If
  End Sub

  Private Sub DataGridView1_Resize(sender As Object, e As EventArgs) _
           Handles DataGridView1.Resize
    DataGridResize()
  End Sub

  Private Sub DataGridView1_Sorted(sender As Object, e As EventArgs) _
           Handles DataGridView1.Sorted
    DataGridColor()
  End Sub

  Function GetStreamReader(ByVal sFilePath As String) As IO.StreamReader
    Return New System.IO.StreamReader(sFilePath, System.Text.Encoding.Default)
  End Function

  Private Function GetDataTableFromFolder(ByVal sFilePath As String, _
           ByVal sFolderPath As String) As Data.DataTable

    Dim iSingleMp3FileSize As Integer = 0
    Dim iSingleMp3Seconds As Integer = 0
    Dim sSingleMp3FilePath As String = _
           Path.Combine(Path.GetDirectoryName(sFilePath), _
           Path.GetFileNameWithoutExtension(sFilePath) & ".mp3")
    If IO.File.Exists(sSingleMp3FilePath) Then
      Dim oMP3Info As New Monotic.Multimedia.MP3.MP3Info(sSingleMp3FilePath)
      iSingleMp3Seconds = oMP3Info.Length
      Dim oFileInfo As New IO.FileInfo(sSingleMp3FilePath)
      iSingleMp3FileSize = oFileInfo.Length
    End If
    Dim bUseSingleMp3 As Boolean = sSingleMp3FilePath <> _
                         "" AndAlso iSingleMp3FileSize > 0

    Dim iStart As Integer = 0
    Dim oTable As New Data.DataTable
    oTable.Columns.Add(New Data.DataColumn("Name"))
    oTable.Columns.Add(New Data.DataColumn("Length", _
                          System.Type.GetType("System.Int64")))
    oTable.Columns.Add(New Data.DataColumn("Start", _
                          System.Type.GetType("System.Int64")))
    oTable.Columns.Add(New Data.DataColumn("Start2"))
    oTable.Columns.Add(New Data.DataColumn("Text"))
    oTable.Columns.Add(New Data.DataColumn("FilePath"))
    oTable.Columns.Add(New Data.DataColumn("Size", _
                          System.Type.GetType("System.Int64")))

    Dim iRows As Integer = GetFileRowsCount(sFilePath)
    Dim iMaxSize As Integer = iRows.ToString().Length

    Dim oStreamReader As System.IO.StreamReader = GetStreamReader(sFilePath)
    Dim iRow As Integer = 0
    Dim sLine As String = oStreamReader.ReadLine()
    Do Until sLine Is Nothing
      Dim oDataRow As DataRow = oTable.NewRow()

      If bUseSingleMp3 Then
        oDataRow("Start") = (iStart / iSingleMp3FileSize) * _
                                   (iSingleMp3Seconds * 1.0)
      Else
        oDataRow("Start") = iStart + (iRow * 0.945) ' 300ms
      End If

      iRow += 1
      Dim sSrcFileBase As String = Microsoft.VisualBasic.Right_
                                        ("000000" & iRow, iMaxSize)
      Dim sSrcFilePath As String = _
               Path.Combine(sFolderPath, sSrcFileBase & ".mp3")
      If IO.File.Exists(sSrcFilePath) Then
        Dim oFileInfo As New IO.FileInfo(sSrcFilePath)
        oDataRow("Size") = oFileInfo.Length
        oDataRow("FilePath") = sSrcFilePath


        If bUseSingleMp3 Then
          oDataRow("Length") = (oFileInfo.Length / iSingleMp3FileSize) * _
                                        (iSingleMp3Seconds * 1.0)
          iStart += oFileInfo.Length
          iStart += 2657 '(2,657 bytes) '300ms.mp3
        Else
          Dim oMP3Info As New Monotic.Multimedia.MP3.MP3Info(sSrcFilePath)
          Dim iLength As Integer = oMP3Info.Length

          oDataRow("Length") = iLength
          iStart += iLength
        End If
      Else
        oDataRow("Size") = 0
        oDataRow("Length") = 0
        oDataRow("FilePath") = ""
      End If

      oDataRow("Start2") = TimeSpan.FromSeconds(oDataRow("Start")).ToString()
      oDataRow("Name") = sSrcFileBase
      oDataRow("Text") = sLine

      oTable.Rows.Add(oDataRow)
      sLine = oStreamReader.ReadLine()
    Loop

    oStreamReader.Close()
    Return oTable
  End Function

  Private Sub btnPlay_Click(sender As Object, e As EventArgs) Handles btnPlay.Click
    PlaySound()
  End Sub

  Private Sub btnDelete_Click(sender As Object, e As EventArgs) Handles btnDelete.Click
    Dim sFilePath As String = DeleteFile("Delete")
    If sFilePath <> "" Then
      UpdateFileGrid()
    Else
      MsgBox("MP3 file does not exist " & sFilePath)
    End If
  End Sub

  Function DeleteFile(ByVal sAction As String) As String
    Dim sFilePath As String = GetSelectedFielPath()
    If IO.File.Exists(sFilePath) = False Then
      Return ""
    End If

    If MsgBox(sAction & " file " & Path.GetFileName(sFilePath) & _
                 "?", MsgBoxStyle.YesNo, sAction & " file") <> vbYes Then
      Return ""
    End If

    PlaySoundStop()

    Try
      IO.File.Delete(sFilePath)
    Catch ex As Exception
      MsgBox("Could Not delete file " & sFilePath & " " & ex.Message)
      Return ""
    End Try

    Return sFilePath
  End Function

  Private Sub btnRegenerate_Click(sender As Object, e As EventArgs) _
           Handles btnReGenerate.Click

    Dim sFilePath As String = DeleteFile("Regenerate")
    If sFilePath = "" Then
      MsgBox("MP3 file does not exist " & sFilePath)
      Exit Sub
    End If

    btnReGenerate.Enabled = False
    My.Application.DoEvents()

    If btnSave.Visible = True Then
      SaveTextFile()
    End If

    Dim sFileName As String = Path.GetFileNameWithoutExtension(sFilePath)
    Dim iProcessRow As Integer = sFileName
    ProcessTextFile(iProcessRow)
    UpdateFileGrid()
    PlaySound()

    btnReGenerate.Enabled = True

  End Sub

  Sub MergeFolder(sFolderPath, sFilePath)

    If System.IO.Directory.Exists(sFolderPath) = False Then
      MsgBox("Folder does Not exist " & sFolderPath)
      Exit Sub
    End If

    If IO.File.Exists(sFilePath) Then
      IO.File.Delete(sFilePath)
    End If

    Dim oProcess As New System.Diagnostics.Process()
    Dim startInfo As New System.Diagnostics.ProcessStartInfo()
    startInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden
    startInfo.FileName = "cmd.exe"
    startInfo.Arguments = "/C copy /b """ & sFolderPath & _
                             "\*.mp3"" """ & sFilePath & """"
    oProcess.StartInfo = startInfo
    oProcess.Start()
    oProcess.WaitForExit()
  End Sub

  Function GetPauseFilePath() As String
    Dim sMp3FolderePath As String = GetFolderPath("mp3")
    Return sMp3FolderePath & "\300ms.mp3"
  End Function

  Private Sub btnChapters_Click(sender As Object, e As EventArgs) _
                                 Handles btnChapters.Click

    Dim sFilePath As String = txtSrcFile.Text
    If sFilePath = "" Then
      MsgBox("Text file is blank")
      Exit Sub
    End If

    If IO.File.Exists(sFilePath) = False Then
      txtSrcFile.Text = ""
      MsgBox("Text file is blank")
      Exit Sub
    End If

    Dim sFolderPath As String = Path.GetDirectoryName(sFilePath)
    Dim sFileName As String = Path.GetFileNameWithoutExtension(sFilePath)

    Dim sSrcFolderPath As String = Path.Combine(sFolderPath, sFileName)
    If System.IO.Directory.Exists(sSrcFolderPath) = False Then
      MsgBox("Source folder Is blank: " & sSrcFolderPath)
      Exit Sub
    End If

    Dim sPauseFilePath As String = GetPauseFilePath()
    If IO.File.Exists(sPauseFilePath) = False Then
      MsgBox("Could not find " & sPauseFilePath)
      Exit Sub
    End If

    Dim sDstFolderPath As String = _
           Path.Combine(sFolderPath, sFileName & "-Chapters")
    If System.IO.Directory.Exists(sDstFolderPath) Then

      'SetFileTags(sDstFolderPath, sFileName)

      Try
        EmptyFolder(sDstFolderPath)
      Catch ex As Exception
        MsgBox("Could not empty folder " & sDstFolderPath)
        Exit Sub
      End Try

      System.Threading.Thread.Sleep(1000)
    End If

    If System.IO.Directory.Exists(sDstFolderPath) = False Then
      System.IO.Directory.CreateDirectory(sDstFolderPath)
    End If

    Dim iRows As Integer = GetFileRowsCount(sFilePath)
    If iRows = 0 Then
      Exit Sub
    End If
    Dim iMaxSize As Integer = iRows.ToString().Length

    btnChapters.Enabled = False
    My.Application.DoEvents()

    Dim iRow As Integer = 0
    Dim oStreamReader As System.IO.StreamReader = GetStreamReader(sFilePath)
    Dim iBlankCount As Integer = 0
    Dim iChapterCount As Integer = 1
    Dim sChapterName As String = "Beginning"
    Dim sLine As String = oStreamReader.ReadLine()
    Do Until sLine Is Nothing
      iRow += 1

      If iBlankCount = 2 AndAlso Trim(sLine) <> "" Then
        sChapterName = PadFileName(Trim(sLine))
        iChapterCount += 1
      End If

      Dim sChapterName2 As String = Microsoft.VisualBasic.Right_
                             ("00" & iChapterCount, 2) & " " & sChapterName
      Dim sChapterFolderPath As String = _
                             Path.Combine(sDstFolderPath, sChapterName2)
      If System.IO.Directory.Exists(sChapterFolderPath) = False Then
        System.IO.Directory.CreateDirectory(sChapterFolderPath)
      End If

      Dim sSrcFileBase As String = _
                            Microsoft.VisualBasic.Right("000000" & iRow, iMaxSize)
                                        
      Dim sSrcFilePath As String = _
                            Path.Combine(sSrcFolderPath, sSrcFileBase & ".mp3")

      If IO.File.Exists(sSrcFilePath) Then
        Dim sDestFilePath As String = _
            Path.Combine(sChapterFolderPath, sSrcFileBase & "0.mp3")
        IO.File.Copy(sSrcFilePath, sDestFilePath)

        sDestFilePath = Path.Combine(sChapterFolderPath, sSrcFileBase & "1.mp3")
        If IO.File.Exists(sDestFilePath) = False Then
          IO.File.Copy(sPauseFilePath, sDestFilePath)
        End If
      End If

      If Trim(sLine) = "" Then
        iBlankCount += 1
      Else
        iBlankCount = 0
      End If

      sLine = oStreamReader.ReadLine()
    Loop

    oStreamReader.Close()

    System.Threading.Thread.Sleep(100)

    For Each sSubFolder As String In IO.Directory.GetDirectories(sDstFolderPath)
      Dim sDestFilePath As String = sSubFolder & ".mp3"
      MergeFolder(sSubFolder, sDestFilePath)
    Next

    For Each sSubFolder As String In IO.Directory.GetDirectories(sDstFolderPath)
      System.IO.Directory.Delete(sSubFolder, True)
    Next

    SetFileTags(sDstFolderPath, sFileName)
    btnChapters.Enabled = True
    MsgBox("Done")

  End Sub

  Private Sub SetFileTags(sDstFolderPath As String, sAlbum As String)
    Dim oFiles As String() = System.IO.Directory.GetFiles(sDstFolderPath)
    For Each sFile In oFiles
      Dim oFileInfo As New System.IO.FileInfo(sFile)
      If oFileInfo.Extension.ToLower() = ".mp3" Then
        Dim oMP3Info As New Monotic.Multimedia.MP3.MP3Info(sFile)
        oMP3Info.ID3v1Tag.Album = Microsoft.VisualBasic.Left(sAlbum, 30)
        oMP3Info.ID3v1Tag.Artist = ""
        oMP3Info.ID3v1Tag.Title = TrimAlbum(oFileInfo.Name)
        oMP3Info.Update()
        'Console.WriteLine(oFileInfo.Name)
      End If
    Next
  End Sub

  Private Sub btn192Bitrate_Click(sender As Object, e As EventArgs) _
                                   Handles btn192Bitrate.Click

    Dim sFilePath As String = txtSrcFile.Text
    If sFilePath = "" Then
      MsgBox("Text file is blank")
      Exit Sub
    End If

    Dim sVolume As String = "-2.5"
    Dim sFolderPath As String = Path.GetDirectoryName(sFilePath)
    Dim sFileName As String = Path.GetFileNameWithoutExtension(sFilePath)

    Dim sChaptersFolderPath As String = Path.Combine_
                               (sFolderPath, sFileName & "-Chapters")
    If System.IO.Directory.Exists(sChaptersFolderPath) = False Then
      MsgBox("Chapters folders is missing")
      Exit Sub
    End If

    Dim sFfmpegFile As String = GetFfmpegFile()
    If IO.File.Exists(sFfmpegFile) = False Then
      MsgBox("ffmpeg.exe file is missing: " & sFfmpegFile)
      Exit Sub
    End If

    btn192Bitrate.Enabled = False
    My.Application.DoEvents()

    Dim s192FolderPath As String = _
           Path.Combine(sFolderPath, sFileName & "-Chapters192")
    If System.IO.Directory.Exists(s192FolderPath) = False Then
      System.IO.Directory.CreateDirectory(s192FolderPath)
    End If

    Dim oFiles As String() = System.IO.Directory.GetFiles(sChaptersFolderPath)
    Dim iFileCount As Integer = 0
    ProgressBar1.Visible = True
    ProgressBar1.Maximum = oFiles.Length

    For Each sInputFilePath As String In oFiles
      iFileCount += 1
      ProgressBar1.Value = iFileCount

      My.Application.DoEvents()
      If bStop Then
        bStop = False
        MsgBox("Stopped Processing at row " & iFileCount)
        Exit For
      End If

      Dim sInputFileName As String = Path.GetFileName(sInputFilePath)
      Dim sOutputFilePath As String = Path.Combine(s192FolderPath, sInputFileName)

      If IO.File.Exists(sOutputFilePath) Then
        IO.File.Delete(sOutputFilePath)
      End If

      Dim sArguments As String = "-i """ & sInputFilePath & """ -af ""volume=" _
               & sVolume & "dB"" -b:a ""192k"" """ & sOutputFilePath & """"
      Dim oProcess As New System.Diagnostics.Process()
      Dim startInfo As New System.Diagnostics.ProcessStartInfo()
      startInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden
      startInfo.FileName = sFfmpegFile
      startInfo.Arguments = sArguments
      oProcess.StartInfo = startInfo
      oProcess.Start()
      oProcess.WaitForExit()

      If IO.File.Exists(sOutputFilePath) = False Then
        Clipboard.SetText(sFfmpegFile & " " & sArguments)
        MessageBox.Show("mp3 file was not created: " & _
               sOutputFilePath & ". Command text is copied to Clipboard.")
      Else
        Dim oFileInfo As New FileInfo(sOutputFilePath)
        If oFileInfo.Length < 10 Then
          Clipboard.SetText(sFfmpegFile & " " & sArguments)
          MessageBox.Show("mp3 file was created but is blank: " _
                   & sOutputFilePath & ". Command text is copied to Clipboard.")
        End If
      End If
    Next

    SetFileTags(s192FolderPath, sFileName)
    btn192Bitrate.Enabled = True
    ProgressBar1.Visible = False
    MsgBox("Done")

  End Sub

  Function PadImageText(ByVal s As String)
    Dim i As Integer = s.IndexOf(" ")
    If i = -1 Then
      Return s
    End If

    If IsNumeric(s.Substring(0, i)) Then
      Return Trim(s.Substring(i))
    End If

    Return s
  End Function

  Private Sub btnMakeVideos_Click(sender As Object, e As EventArgs) _
                                   Handles btnMakeVideos.Click

    Dim sFilePath As String = txtSrcFile.Text
    If sFilePath = "" OrElse IO.File.Exists(sFilePath) = False Then
      txtSrcFile.Text = ""
      MsgBox("Source text file Is blank")
      Exit Sub
    End If

    Dim sImageFilePath As String = txtImageFile.Text
    'If sImageFilePath = "" OrElse IO.File.Exists(sImageFilePath) = False Then
    '  txtImageFile.Text = ""
    '  MsgBox("Image file Is blank")
    '  Exit Sub
    'End If

    'Dim sVolume As String = "-2.5"
    Dim sFolderPath As String = Path.GetDirectoryName(sFilePath)
    Dim sFileName As String = Path.GetFileNameWithoutExtension(sFilePath)

    Dim sChaptersFolderPath As String = Path.Combine_
                    (sFolderPath, sFileName & "-Chapters")
    If System.IO.Directory.Exists(sChaptersFolderPath) = False Then
      MsgBox("Chapters folders is missing: " & sChaptersFolderPath)
      Exit Sub
    End If

    Dim sFfmpegFile As String = GetFfmpegFile()
    If IO.File.Exists(sFfmpegFile) = False Then
      MsgBox("ffmpeg.exe file is missing: " & sFfmpegFile)
      Exit Sub
    End If

    If MsgBox("Creating video files might take hours. " &
        "A video mp4 file will be created for each chapter file. " &
        "Existing video files will be skipped. " &
       " Are you sure you want to do this?", vbYesNo) <> vbYes Then
      Exit Sub
    End If

    btnMakeVideos.Enabled = False
    My.Application.DoEvents()

    Dim sVideoFolderPath As String = _
           Path.Combine(sFolderPath, sFileName & "-Videos")
    If System.IO.Directory.Exists(sVideoFolderPath) = False Then
      System.IO.Directory.CreateDirectory(sVideoFolderPath)
    End If

    Dim oFiles As String() = System.IO.Directory.GetFiles(sChaptersFolderPath)
    Dim iFileCount As Integer = 0
    ProgressBar1.Visible = True
    ProgressBar1.Maximum = oFiles.Length

    For Each sInputFilePath As String In oFiles
      iFileCount += 1
      ProgressBar1.Value = iFileCount

      My.Application.DoEvents()
      If bStop Then
        bStop = False
        MsgBox("Stopped Processing at row " & iFileCount)
        Exit For
      End If

      Dim sOutputFileName As String = _
               Path.GetFileNameWithoutExtension(sInputFilePath) & ".mp4"
      Dim sOutputFilePath As String = _
               Path.Combine(sVideoFolderPath, sOutputFileName)

      'If IO.File.Exists(sOutputFilePath) Then
      '  IO.File.Delete(sOutputFilePath)
      'End If

      If IO.File.Exists(sOutputFilePath) = False Then

        Dim sInputFilePath2 As String = sImageFilePath
        Dim sImageText As String = _
                   Path.GetFileNameWithoutExtension(sInputFilePath)
        Dim sTempImageFilePath As String = ""

        If chkImageText.Checked Then
          Try
            'Try to add text image
            sTempImageFilePath = IO.Path.Combine(GetTempFolder(), _
                                            GetGuidFileName("png"))
            AddTextToImage(sImageFilePath, PadImageText(sImageText), _
                                      sTempImageFilePath, 100)
            sInputFilePath2 = sTempImageFilePath
          Catch ex As Exception
            'Ignore if failed
          End Try
        ElseIf sImageFilePath = "" Then
          sTempImageFilePath = IO.Path.Combine(GetTempFolder(), _
                                        GetGuidFileName("png"))
          AddTextToImage(sImageFilePath, PadImageText(sImageText), _
                                  sTempImageFilePath, 100)
          sInputFilePath2 = sTempImageFilePath
        End If

        Dim sArguments As String = "-loop 1 -i """ & _
               sInputFilePath2 & """ -i """ & sInputFilePath & _
               """ -c:v libx264 -vf ""pad=ceil(iw/2)*2:ceil(ih/2)*2"" _
                   -tune stillimage -c:a aac -b:a 192k -pix_fmt yuv420p _
                   -shortest """ & sOutputFilePath & """"
        Dim oProcess As New System.Diagnostics.Process()
        Dim startInfo As New System.Diagnostics.ProcessStartInfo()
        startInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Minimized
        startInfo.FileName = sFfmpegFile
        startInfo.Arguments = sArguments
        oProcess.StartInfo = startInfo
        oProcess.Start()
        oProcess.WaitForExit()

        If IO.File.Exists(sOutputFilePath) = False Then
          Clipboard.SetText(sFfmpegFile & " " & sArguments)
          MessageBox.Show("Video file was not created: " _
                   & sOutputFilePath & ". Command text is copied to Clipboard.")
        Else
          Dim oFileInfo As New FileInfo(sOutputFilePath)
          If oFileInfo.Length < 10 Then
            Clipboard.SetText(sFfmpegFile & " " & sArguments)
            MessageBox.Show("Video file was created but is blank: _" _
                       & sOutputFilePath & ". Command text is copied to Clipboard.")
          End If
        End If

        If IO.File.Exists(sTempImageFilePath) Then
          'Cleanup
          Try
            IO.File.Delete(sTempImageFilePath)
          Catch ex As Exception
            'Ignore
          End Try
        End If

      End If
    Next

    ProgressBar1.Visible = False
    btnMakeVideos.Enabled = True
    MsgBox("Done")

  End Sub

  Sub AddTextToImage(ByVal imagePath As String, ByVal text As String, _
                      ByVal savePath As String, ByVal bottomMargin As Single)

    ' Create a new blank image if the imagePath is empty
    Dim img As Image
    If String.IsNullOrEmpty(imagePath) Then
      img = New Bitmap(800, 800)
      Using g As Graphics = System.Drawing.Graphics.FromImage(img)
        g.Clear(Color.White) ' Set the background color to white
      End Using
    Else
      ' Load the existing image
      img = Image.FromFile(imagePath)
    End If

    ' Create a Graphics object from the image
    Dim graphics As Graphics = Graphics.FromImage(img)

    ' Create a brush for the text
    Dim brush As New SolidBrush(Color.Black)

    ' Create a font for the text. Start with a large size.
    Dim fontSize As Single = 72
    Dim font As New Font("Arial", fontSize)

    ' Calculate the size of the text
    Dim textSize As SizeF = graphics.MeasureString(text, font)

    ' If the text is too wide, decrease the font size 
       ' until it fits the image's width
    While textSize.Width > img.Width
      fontSize -= 1
      font = New Font("Arial", fontSize)
      textSize = graphics.MeasureString(text, font)
    End While

    If bottomMargin + 100 > img.Height Then
      bottomMargin = 0
    End If

    ' Define where the text will be placed 
       ' (bottom and center of the image, accounting for margin)
    Dim rect As New RectangleF((img.Width - textSize.Width) / 2, _
       img.Height - textSize.Height - bottomMargin, textSize.Width, textSize.Height)

    ' Draw the string onto the image
    graphics.DrawString(text, font, brush, rect)

    ' Save the image
    img.Save(savePath, System.Drawing.Imaging.ImageFormat.Png)

    ' Clean up
    brush.Dispose()
    graphics.Dispose()
    img.Dispose()
  End Sub

  Sub MergeMp4Files(ByVal sVideoFolderPath As String, _
                     ByVal sOutputFilePath As String, ByVal sFfmpegFile As String)

    Dim sFilePath As String = txtSrcFile.Text
    Dim sFolderPath As String = Path.GetDirectoryName(sFilePath)
    Dim sFileName As String = Path.GetFileNameWithoutExtension(sFilePath)
    Dim sTempFolderPath As String = Path.Combine(sFolderPath, sFileName & _
           "_one_video_" & DateTime.Now.ToString("yyyyMM_ddHHmmss"))

    IO.Directory.CreateDirectory(sTempFolderPath)

    Dim sTextFilePath As String = Path.Combine(sTempFolderPath, "Files.txt")
    Dim sw As New StreamWriter(sTextFilePath, False)

    For Each sPath As String In IO.Directory.GetFiles(sVideoFolderPath)
      If Path.GetExtension(sPath) = ".mp4" Then
        Dim sName As String = Trim(Microsoft.VisualBasic.Left_
                   (Path.GetFileNameWithoutExtension(sPath), 3))
        Dim sFName As String = sName & ".mp4"
        Dim sToPath As String = Path.Combine(sTempFolderPath, sFName)
        File.Copy(sPath, sToPath)
        sw.WriteLine("file '" & sFName & "'")
      End If
    Next

    sw.Close()

    Dim oProcess As New System.Diagnostics.Process()
    Dim startInfo As New System.Diagnostics.ProcessStartInfo()
    startInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Maximized
    startInfo.FileName = sFfmpegFile
    startInfo.Arguments = "-f concat -i """ & sTextFilePath & """ _
                              -c copy """ & sOutputFilePath & """"
    oProcess.StartInfo = startInfo
    oProcess.Start()
    oProcess.WaitForExit()

    System.Threading.Thread.Sleep(1000)

    Try
      EmptyFolder(sTempFolderPath)
    Catch ex As Exception
      MsgBox("Could not empty folder " & sTempFolderPath)
      Exit Sub
    End Try

  End Sub

  Private Sub btnMakeVideo_Click(sender As Object, e As EventArgs) _
                                  Handles btnMakeVideo.Click

    Dim sFilePath As String = txtSrcFile.Text
    If sFilePath = "" OrElse IO.File.Exists(sFilePath) = False Then
      MsgBox("Source text file Is blank")
      Exit Sub
    End If

    Dim sFfmpegFile As String = GetFfmpegFile()
    If IO.File.Exists(sFfmpegFile) = False Then
      MsgBox("ffmpeg.exe file is missing: " & sFfmpegFile)
      Exit Sub
    End If

    Dim sFolderPath As String = Path.GetDirectoryName(sFilePath)
    Dim sFileName As String = Path.GetFileNameWithoutExtension(sFilePath)
    Dim sOutputFilePath As String = sFolderPath & "\" & sFileName & ".mp4"

    If IO.File.Exists(sOutputFilePath) Then
      MsgBox("Single video already exists. _
           If you want to re-create it please delete it manually: " & sOutputFilePath)
      Exit Sub
    End If

    Dim sVideoFolderPath As String = _
                 Path.Combine(sFolderPath, sFileName & "-Videos")
    If System.IO.Directory.Exists(sVideoFolderPath) _
          AndAlso System.IO.Directory.GetFiles(sVideoFolderPath).Length > 1 Then
      'Chapter MP4 already exists - merge them instead of creating new mp4 file
      MergeMp4Files(sVideoFolderPath, sOutputFilePath, sFfmpegFile)
      Exit Sub
    End If

    Dim sImageFilePath As String = txtImageFile.Text
    If sImageFilePath = "" Then
      MsgBox("Image file Is blank")
      Exit Sub
    End If

    Dim sInputFilePath As String = sFolderPath & "\" & sFileName & ".mp3"
    If System.IO.File.Exists(sInputFilePath) = False Then
      MsgBox("MP3 file is is missing " & sInputFilePath)
      Exit Sub
    End If

    Dim oProcess As New System.Diagnostics.Process()
    Dim startInfo As New System.Diagnostics.ProcessStartInfo()
    startInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Maximized
    startInfo.FileName = sFfmpegFile
    startInfo.Arguments = "-loop 1 -i """ & sImageFilePath & _
            """ -i """ & sInputFilePath & """ -c:v libx264 -tune stillimage _
            -c:a aac -b:a 192k -pix_fmt yuv420p -shortest """ & sOutputFilePath & """"
    oProcess.StartInfo = startInfo
    oProcess.Start()
    oProcess.WaitForExit()

    MsgBox("Done")
  End Sub

  Private Function TrimAlbum(ByVal s As String)
    If s.Length > 30 Then
      Return s.Substring(0, 30)
    Else
      Return s
    End If
  End Function

  Private Sub EmptyFolder(ByVal sFolder As String)

    For Each sFolderPath As String In IO.Directory.GetDirectories(sFolder)
      System.IO.Directory.Delete(sFolderPath, True)
    Next

    For Each sFilePath As String In IO.Directory.GetFiles(sFolder)
      File.Delete(sFilePath)
    Next

  End Sub

  Public Function PadFileName(ByVal s As String) As String
    s = Replace(s, "<", "")
    s = Replace(s, ">", "")
    s = Replace(s, ":", "-")
    s = Replace(s, """", "")
    s = Replace(s, "/", "")
    s = Replace(s, "\", "")
    s = Replace(s, "?", "")
    s = Replace(s, "'", "")
    s = Replace(s, ChrW(65533), "")
    's = Replace(s, " ", "_")
    Return Replace(s, "*", "")
  End Function

  Private Sub DataGridView1_CellClick(sender As Object, _
           e As DataGridViewCellEventArgs) Handles DataGridView1.CellClick
    SetupLineText()
  End Sub

  Private Sub DataGridView1_KeyUp(sender As Object, e As KeyEventArgs) _
               Handles DataGridView1.KeyUp
    SetupLineText()
    If chkPlayOnKeyUp.Checked Then
      Dim sFilePath As String = GetSelectedFielPath()
      If sFilePath <> "" Then
        PlaySound(sFilePath)
      End If
    End If
  End Sub

  Function GetSelectedRowIndex()
    If DataGridView1.SelectedRows.Count > 0 Then
      Return DataGridView1.SelectedRows(0).Index
    ElseIf DataGridView1.SelectedCells.Count > 0 Then
      Return DataGridView1.SelectedCells(0).RowIndex
    End If
    Return -1
  End Function

  Sub SetupLineText()
    Dim iSelectedRowIndex As Integer = GetSelectedRowIndex()
    If iSelectedRowIndex <> -1 Then
      Dim oRow As DataGridViewRow = DataGridView1.Rows(iSelectedRowIndex)
      txtLine.Text = oRow.Cells("Text").Value
      Me.Text = "Audio Book Creator - " & oRow.Cells("Name").Value
    Else
      Me.Text = "Audio Book Creator"
    End If
  End Sub

  Function GetSelectedFielPath() As String
    Dim iSelectedRowIndex As Integer = GetSelectedRowIndex()
    If iSelectedRowIndex <> -1 Then
      Dim oRow As DataGridViewRow = DataGridView1.Rows(iSelectedRowIndex)
      Return oRow.Cells("FilePath").Value
    End If
    Return ""
  End Function

  Private Sub txtLine_TextChanged(sender As Object, e As EventArgs) _
                                   Handles txtLine.TextChanged
    Dim iSelectedRowIndex As Integer = GetSelectedRowIndex()
    If iSelectedRowIndex <> -1 Then
      Dim oRow As DataGridViewRow = DataGridView1.Rows(iSelectedRowIndex)
      oRow.Cells("Text").Value = txtLine.Text
      btnSave.Visible = True
    End If
  End Sub

  Private Sub btnSave_Click(sender As Object, e As EventArgs) Handles btnSave.Click
    SaveTextFile()
  End Sub

  Sub SaveTextFile()
    Dim sFilePath As String = txtSrcFile.Text
    Dim oEncoding As System.Text.Encoding = System.Text.Encoding.ASCII
    Dim sBackupFilePath As String = ""

    If System.IO.File.Exists(sFilePath) Then
      oEncoding = DetectEncoding(sFilePath)
      Dim sBackupFileName As String = _
             Path.GetFileNameWithoutExtension(sFilePath) & "_" & _
             DateTime.Now.ToString("yyyyMM_ddHHmmss") & Path.GetExtension(sFilePath)
      sBackupFilePath = Path.Combine(Path.GetDirectoryName(sFilePath), _
                                          sBackupFileName)
      File.Move(sFilePath, sBackupFilePath)
    End If

    Dim sw As New StreamWriter(sFilePath, False, oEncoding)
    For iRow = 0 To DataGridView1.RowCount - 1
      Dim oRow As DataGridViewRow = DataGridView1.Rows(iRow)
      If oRow.IsNewRow = False Then
        Dim sText As String = oRow.Cells("Text").Value
        Dim sName As String = oRow.Cells("Name").Value
        sw.WriteLine(sText)
      End If
    Next

    sw.Close()

    If sBackupFilePath <> "" Then
      If chkBackupFile.Checked Then
        Dim sBackupFolder As String = _
                   Path.Combine(Path.GetDirectoryName(sFilePath), _
                   Path.GetFileNameWithoutExtension(sFilePath) & "_backup")
        If IO.Directory.Exists(sBackupFolder) = False Then
          IO.Directory.CreateDirectory(sBackupFolder)
        End If
        Dim sNewBackupFilePath = Path.Combine(sBackupFolder, _
                                        Path.GetFileName(sBackupFilePath))
        File.Move(sBackupFilePath, sNewBackupFilePath)
      Else
        File.Delete(sBackupFilePath)
      End If
    End If

    btnSave.Visible = False
  End Sub

  Function DetectEncoding(filePath As String) As System.Text.Encoding
    Dim encoding As System.Text.Encoding = _
       System.Text.Encoding.Default ' Fallback to the default encoding 
                                    ' if BOM is not found

    Using fs As New FileStream(filePath, FileMode.Open, FileAccess.Read)
      If fs.Length >= 2 Then
        Dim bom(3) As Byte
        fs.Read(bom, 0, 3)

        If bom(0) = &HEF AndAlso bom(1) = &HBB AndAlso bom(2) = &HBF Then
          encoding = System.Text.Encoding.UTF8
        ElseIf bom(0) = &HFF AndAlso bom(1) = &HFE Then
          encoding = System.Text.Encoding.Unicode
        ElseIf bom(0) = &HFE AndAlso bom(1) = &HFF Then
          encoding = System.Text.Encoding.BigEndianUnicode
        ElseIf bom(0) = &H0 AndAlso bom(1) = &H0 AndAlso bom(2) = _
                               &HFE AndAlso bom(3) = &HFF Then
          encoding = System.Text.Encoding.UTF32
        End If
      End If
    End Using

    Return encoding
  End Function

  Private Sub btnSilence_Click(sender As Object, e As EventArgs) _
                                Handles btnSilence.Click
    txtLine.Text = "{{" & selSilence.SelectedItem.ToString() & ".mp3}}"
    btnSave.Visible = True
  End Sub

  Private Sub txtLine_MouseWheel(sender As Object, e As MouseEventArgs) _
                                  Handles txtLine.MouseWheel

    If Control.ModifierKeys = Keys.Control Then
      Dim currentSize As Single = txtLine.Font.Size
      Dim newSize As Single

      If e.Delta > 0 Then
        ' Mouse wheel was moved up, increase font size
        newSize = currentSize + 1
      Else
        ' Mouse wheel was moved down, decrease font size
        newSize = Math.Max(currentSize - 1, 1)
      End If

      txtLine.Font = _
                   New Font(txtLine.Font.FontFamily, newSize, txtLine.Font.Style)
    End If

  End Sub

  Private Sub btnMerge_Click(sender As Object, e As EventArgs) Handles btnMerge.Click
    Dim sFilePath As String = txtSrcFile.Text
    If sFilePath = "" OrElse IO.File.Exists(sFilePath) = False Then
      MsgBox("Text file is blank")
      Exit Sub
    End If

    Dim sFolderPath As String = Path.GetDirectoryName(sFilePath)
    Dim sFileName As String = Path.GetFileNameWithoutExtension(sFilePath)
    Dim sChaptersFolderPath As String = _
                               Path.Combine(sFolderPath, sFileName & "-Chapters")

    If IO.Directory.Exists(sChaptersFolderPath) = False Then
      MsgBox("Chapters folder does not exist: " & sChaptersFolderPath)
      Exit Sub
    End If

    Dim sDestFilePath As String = sFolderPath & "\" & sFileName & ".mp3"

    If IO.File.Exists(sDestFilePath) Then
      IO.File.Delete(sDestFilePath)
    End If

    MergeFolder(sChaptersFolderPath, sDestFilePath)

    Dim oMP3Info As New Monotic.Multimedia.MP3.MP3Info(sDestFilePath)
    txtText.Text = "Created Single MP3 File with Length: " & oMP3Info.Length

    MsgBox("Done")
  End Sub

  Private Sub DeleteFolder(ByVal sFolderPath As String)
    If IO.Directory.Exists(sFolderPath) = False Then
      Exit Sub
    End If

    Dim oFiles As String() = System.IO.Directory.GetFiles(sFolderPath)
    For Each sFile In oFiles
      IO.File.Delete(sFile)
    Next

    IO.Directory.Delete(sFolderPath)

  End Sub

  Private Sub btnChangeVolume_Click(sender As Object, e As EventArgs) _
                                     Handles btnChangeVolume.Click

    Dim sFilePath As String = txtSrcFile.Text
    If sFilePath = "" Then
      MsgBox("Text file is blank")
      Exit Sub
    End If

    Dim sFolderPath As String = Path.GetDirectoryName(sFilePath)
    Dim sFileName As String = Path.GetFileNameWithoutExtension(sFilePath)

    Dim s192FolderPath As String = Path.Combine(sFolderPath, _
                                      sFileName & "-Chapters192")
    If System.IO.Directory.Exists(s192FolderPath) = False Then
      MsgBox("Chapters192 folder does not exist: " & s192FolderPath)
      Exit Sub
    End If

    Dim oChangeVolume As New frmChangeVolume
    oChangeVolume.s192FolderPath = s192FolderPath
    oChangeVolume.ShowDialog()
  End Sub

  Private Sub btnYouTubeIndex_Click(sender As Object, e As EventArgs) _
                                     Handles btnYouTubeIndex.Click

    Dim sFilePath As String = txtSrcFile.Text
    If sFilePath = "" Then
      MsgBox("Text file is blank")
      Exit Sub
    End If

    Dim sFolderPath As String = Path.GetDirectoryName(sFilePath)
    Dim sFileName As String = Path.GetFileNameWithoutExtension(sFilePath)
    Dim sChaptersFolderPath As String = _
           Path.Combine(sFolderPath, sFileName & "-Chapters")
    If System.IO.Directory.Exists(sChaptersFolderPath) = False Then
      MsgBox("Chapters folder does not exist: " & sChaptersFolderPath)
      Exit Sub
    End If

    Dim oForm As New frmYouTube
    oForm.sChaptersFolderPath = sChaptersFolderPath
    oForm.ShowDialog()
  End Sub

  Private Sub btnApiKeyShow_Click(sender As Object, e As EventArgs) _
                                   Handles btnApiKeyShow.Click
    If txtApiKey.PasswordChar = "*" Then
      txtApiKey.PasswordChar = ""
    Else
      txtApiKey.PasswordChar = "*"
    End If
  End Sub

  Private Sub selHighlight_SelectedIndexChanged(sender As Object, _
               e As EventArgs) Handles selHighlight.SelectedIndexChanged
    DataGridColor()
  End Sub

  Private Sub urlApiKey_LinkClicked(sender As Object, _
        e As LinkLabelLinkClickedEventArgs) Handles urlApiKey.LinkClicked
    Process.Start(New ProcessStartInfo("https://beta.elevenlabs.io"))
  End Sub
End Class

History

  • 23rd May, 2023: Version 1 created

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)