Android Question Parse number into words for sexy female voice

Danamo

Member
Licensed User
Longtime User
I have an app which speaks to the user in a sexy female voice. Instead of a voice synthesizer, I have mp3 files of various words recorded by an actual sexy female, and use media player to output the audio as spoken sentences. I build up sentences from individual words as an array of the filenames and then use media player to play the array one word /file at a time.

This may be rather clunky, but it does work. (I'm open to improved alternative suggestions)

When it comes to speaking a number, I have mp3 files such as 1.mp3, 2.mp3, 13.mp3, 20.mp3, hundred.mp3, and so on which contain the spoken sounds for all the needed number words.

To enunciate, for example, the number 1024.6, this gets parsed into 6 separate audio files to play: "1.mp3", "thousand.mp3", "20.mp3", "4 .mp3", "point.mp3", "6,mp3" just as the number would normally be spoken as "one thousand twenty four point six"

Likewise, the number 913 is spoken as "nine hundred thirteen."

My code for doing this is as follows:

B4X:
Public Sub Number(num As Double)
    'Words() is a  global array which will hold the sound filenames.
    'WordCount is a global integer used as the array pointer and counter
    Dim x, y As Double
    WordCount = 0  
    If num >= 1000 Then
        x=Floor(num/1000)
        y = x*1000
        Words(WordCount)=x
        WordCount=WordCount+1
        Words(WordCount)="thousand"
        num = num -y
    End If
    If  num >= 100 Then
        x=Floor(num/100)
        y = x*100
        Words(WordCount)=x
        WordCount=WordCount+1
        Words(WordCount)="hundred"
        num = num -y
    End If
    If num>19 Then 'This gets 20, 30, 40.... 90
        x=Floor(num/10)
        y = x*10
        WordCount=WordCount+1
        Words(WordCount)=y
        num=num-y
    End If
    'the remainder is 1 through 19
    x=Floor(num)
    y=num-x
    WordCount=WordCount+1
    Words(WordCount)=x
    num=num-x
    If num > 0 Then
        WordCount=WordCount+1
        Words(WordCount)="point"
        num=Round2(num,2)
        num = Floor(num*100)
        x=Floor(num/10)
        WordCount=WordCount+1
        Words(WordCount)=x
        x=num-(x*10)
        If x >0 Then
            x=Floor(x)
            WordCount=WordCount+1
            Words(WordCount)=x
        End If
    End If
    For i=0 To WordCount
        Words(i)=Words(i) & ".mp3" 'adds the .mp3 extension to the sound filenames
    Next
End Sub
Now, I know that the expert coders here are either gasping in horror or laughing at my crude, inefficient method for translating numbers into audio filenames to generate speech like this. Hey, I'm dumb, okay? I have to break things down step by step to figure them out! That's why I am here, asking if someone can point me in a better, more elegant direction, using modulus in a loop or something, another way to parse numbers and concatenate the audio files to accomplish this same purpose?

As always, I am willing to DONATE to whoever has the most helpful answer to my question.
 

Roycefer

Well-Known Member
Licensed User
Longtime User
If your code does what you want it to do, then it is "correct".

However, this might be a little easier to extend in the future. This is a full B4J program.
B4X:
Sub Process_Globals
    Private fx As JFX
    Private MainForm As Form
End Sub

Sub AppStart (Form1 As Form, Args() As String)
    'MainForm = Form1
    'MainForm.RootPane.LoadLayout("Layout1") 'Load the layout file.
    'MainForm.Show
    Dim testnums() As Double = Array As Double(4.35, 41.42, 0.95, 10.95, 4293.14)
    For Each testnum As Double In testnums
        Log(testnum)
        Dim l As List = NumberToStringList(testnum)
        For Each k As String In l
            Log(TAB & k)
        Next
    Next
End Sub

'Return true to allow the default exceptions handler to handle the uncaught exception.
Sub Application_Error (Error As Exception, StackTrace As String) As Boolean
    Return True
End Sub

Sub NumberToStringList(d As Double) As List
    'break the number to integer and fraction portions
    Dim numstring As String = d
    Dim fraction As String
    Dim integer As String
    If numstring.Contains(".") Then
        fraction = Regex.Split("\.",numstring)(1)
        integer = Regex.Split("\.",numstring)(0)
    Else
        integer = numstring
    End If
  
    'create a list of fraction words
    Dim fractionList As List
    fractionList.Initialize
    If fraction.Length>0 Then
        fractionList.Add("point")
        For j = 0 To fraction.Length-1
            fractionList.Add(fraction.CharAt(j))
        Next
    End If
  
    'create a list of integer words
    Dim integerList As List
    integerList.Initialize
    For j = integer.Length-1 To 0 Step -1
        If j==integer.Length-1 Then
            If integer.Length>1 And integer.CharAt(integer.Length-2)=="1".CharAt(0) Then
                integerList.InsertAt(0,integer.SubString(integer.Length-2))
                j = j - 1
            Else if (integer.Length==1) Or (integer.Length>1 And integer.CharAt(j)<>"0".CharAt(0)) Then
                integerList.InsertAt(0,integer.SubString(j))
            End If
        Else If j==integer.Length-2 Then
            integerList.InsertAt(0,integer.SubString2(j,j+1) & "0")
        Else If j==integer.Length-3 Then
            integerList.InsertAt(0,"hundred")
            integerList.InsertAt(0,integer.SubString2(j,j+1))
        Else If j==integer.Length-4 Then      
            integerList.InsertAt(0,"thousand")
            integerList.InsertAt(0,integer.SubString2(j,j+1))
        End If
    Next
  
    'concatenate the integer and fraction word lists and return the result
    Dim res As List
    res.Initialize
    res.AddAll(integerList)
    res.AddAll(fractionList)
    Return res
End Sub
 
Last edited:
Upvote 0

Danamo

Member
Licensed User
Longtime User
If your code does it what you want it to do, then it is "correct".

However, this might be a little easier to extend in the future. This is a full B4J program.
B4X:
Nice, I like it. Thanks for taking the time to respond to my question.

So, your approach is treating the number as a string, and splitting out the various numbers based on their position in the string? I also tried doing it that way, but my efforts were much more awkward and convoluted. So I gave up and did it by arithmetic instead. That's the difference between an artist (you) and and a hack (me!)

Even though any code leading to the desired result may be "correct" some are more correct than others. There's something I call "elegance." I might not have it, but I can recognize it when I see it. Thanks for helping to expand my awareness of the art of coding.

I don't see a "donate" button with your post, but I'd be happy to buy you a drink or something for your help.
 
Last edited:
Upvote 0

Roycefer

Well-Known Member
Licensed User
Longtime User
Yes, you're right about elegance. It has mostly to do with taking advantage of the underlying structure of a problem. For example, this solution is easily extensible to handle numbers with many more digits. The main loop for the integer portion can be placed inside another loop that deals with groups of three digits at a time, appending "thousand" or "million" or "billion" as necessary.
 
Upvote 0

Roycefer

Well-Known Member
Licensed User
Longtime User
Here is a better solution with those aforementioned extensions implemented and some bugs fixed.
B4X:
Sub Process_Globals
    Private fx As JFX
    Private MainForm As Form
End Sub

Sub AppStart (Form1 As Form, Args() As String)
    'MainForm = Form1
    'MainForm.RootPane.LoadLayout("Layout1") 'Load the layout file.
    'MainForm.Show
    Dim testnums() As Double = Array As Double(1093024,1004,1024,104,4.35,41.42,0.95,10.95,4293.14)
    For Each testnum As Double In testnums
        Log(testnum)
        Dim l As List = NumberToStringList(testnum)
        For Each k As String In l
            Log(TAB & k)
        Next
    Next
End Sub

'Return true to allow the default exceptions handler to handle the uncaught exception.
Sub Application_Error (Error As Exception, StackTrace As String) As Boolean
    Return True
End Sub

Sub NumberToStringList(d As Double) As List
    Dim PowersOfTen() As String = Array As String("thousand", "million", "billion", "trillion", "quadrillion", "quintillion", "sextillion", "septillion")
    'Convert the number to a string with commas
    Dim numstring As String = NumberFormat2(d,1,20,0,True)
    Dim fraction As String
    Dim integer As String
    If numstring.Contains(".") Then 
        fraction = Regex.Split("\.",numstring)(1)
        integer = Regex.Split("\.",numstring)(0)
    Else
        integer = numstring
    End If
   
    'Process the fractional part
    Dim fractionList As List
    fractionList.Initialize
    If fraction.Length>0 Then
        fractionList.Add("point")
        For j = 0 To fraction.Length-1
            fractionList.Add(fraction.CharAt(j))
        Next
    End If
   
    'Process the integer part
    Dim integerList As List
    integerList.Initialize
    'Break the integer into groups of 3 digits
    Dim groups() As String = Regex.Split(",",integer)
    For k = groups.Length-1 To 0 Step -1
        'Cycle through the groups from least significant to most significant
        integer = groups(k)
        If k<groups.Length-1 And integer<>"000" Then
            'Add the "PowersOfTen" word, if needed
            integerList.InsertAt(0,PowersOfTen(groups.Length-1-k-1))
        End If
        For j = integer.Length-1 To 0 Step -1
            'Cycle through the digits of the group from least significant to most significant
            If j==integer.Length-1 Then
                'The ones' place
                If integer.Length>1 And integer.CharAt(integer.Length-2)=="1".CharAt(0) Then
                    integerList.InsertAt(0,integer.SubString(integer.Length-2))
                    j = j - 1
                Else If (integer.Length==1) Or (integer.Length>1 And integer.CharAt(j)<>"0".CharAt(0)) Then
                    integerList.InsertAt(0,integer.SubString(j))
                End If
            Else If j==integer.Length-2 Then
                'The tens' place
                If integer.CharAt(j)<>"0".CharAt(0) Then
                    integerList.InsertAt(0,integer.SubString2(j,j+1) & "0")
                End If
            Else If j==integer.Length-3 Then
                'The hundreds' place
                If integer.CharAt(j)<>"0".CharAt(0) Then
                    integerList.InsertAt(0,"hundred")
                    integerList.InsertAt(0,integer.SubString2(j,j+1))
                End If
            End If
        Next
    Next
   
    'Concatenate the integer words and fraction words and return the result
    Dim res As List
    res.Initialize
    res.AddAll(integerList)
    res.AddAll(fractionList)
    Return res
End Sub
 
Upvote 0

Danamo

Member
Licensed User
Longtime User
Excellent! I tested it with a wide range of numbers and it works beautifully and flawlessly.

Additionally, your use of nested FORs and IFs, and comments makes this a textbook example of how to write code in a clear and concise manner easily understood by others.

Thank You.
 
Upvote 0
Top