Using Numpy to build WAV files
Tags: opensource, python
Published: 2020-11-01 23:26:01

I found a fun little tool that converts text into Morse Code with audio. {% github nimit2801/MorseCodeMessenger no-readme %} It works by first declaring a dictionary of letters, numbers and symbols with their corresponding definitions of dots and dashes. The program asks the user for input; iterates through the provided string one character at a time; looks up the character's definition; and saves the result to a new encoded string. The program includes two wav files: a dot and a dash. The program iterates through the saved encoded string of dots and dashes and plays the appropriate wav file. So it can encode text into sound, but the issue was can it [decode sound into text](https://github.com/nimit2801/MorseCodeMessenger/issues/2)?

I have a little bit of experience making command line tools, but I have never worked with sound before. My plan to tackle this issue was:
* figure out how to read a wav file
* create a wav file from multiple smaller ones
* figure out how to find patterns in a wav file

After a little bit of research I found that a straightforward way to read a wav file was to use [scipy.io.wavfile.read](https://docs.scipy.org/doc/scipy/reference/generated/scipy.io.wavfile.read.html#scipy.io.wavfile.read). The method takes a file location to a wav file and returns a sample rate as an integer and a numpy array. The opposite method [scipy.io.wavfile.write](https://docs.scipy.org/doc/scipy/reference/generated/scipy.io.wavfile.write.html#scipy.io.wavfile.write) takes a file location, a sample rate and a numpy array and creates a wav file. After playing around with these functions for a while, I was able to read the dot and dash wav files and create a large numpy array by appending copies of the dot and dash arrays.
```python
dt = np.dtype('uint8')
blank = np.array([0] * 800, dt)   # 0.1s
padding = np.array([0] * 240, dt) # 0.03s
samplerate, dot = wavfile.read(DOT)
samplerate, dash = wavfile.read(DASH)
dot = np.append(dot, padding.copy())
dash = np.append(dash, padding.copy())
wav_data = np.empty((0,0), dt)
for tune in self.encoded:
  np_data = dot.copy() if tune == '.' else dash.copy() if tune == '-' else blank.copy()
  wav_data = np.append(wav_data.copy(), np_data) 
location = f'{self.filename}.wav'
wavfile.write(location, samplerate, wav_data)
```
The last thing I needed the tool to do was read a wav file and recognize patterns, specifically a dot, a dash, or blank silence. Perhaps there is a better method, but I decided that I would have the program read the wav file and look for specific values in the returned array at specific locations. This isn't a particularly elegant solution as it mainly involved checking an arbitrary location. 
```python
samplerate, wav = wavfile.read(f'{self.filename}.wav')
end = wav.shape[0]
self.encoded = ''
ptr = 0
while ptr < end: 
  if wav[ptr] == 0:
    ptr += 800      # size of blank array
    self.encoded += ' '
  elif wav[ptr + 800] == 128:
    ptr += 1120     # size of dot array
    self.encoded += '.'
  else:
    ptr += 2080     # size of dash array
    self.encoded += '-'
```
The result is a string of dots, dashes and spaces which could be passed to the original functions of the tool. 
The last thing I wanted to do, was give the user a way to specify which functions to call from the command line. So I turned to [argparse](https://docs.python.org/3/library/argparse.html) and implemented the following flags:

Optional flags | Purpose| Example
--|--|--
`-w`<br>`--wav` | Create a Morse Code audio file | At the prompts, enter a message *Hello World* and a name *hi*. An audio file will be created and saved as `hi.wav`
`-p`<br>`--play` | Play a Morse Code audio file | At the prompt, enter *hi* and if `hi.wav` exists, the audio will be played
`-r`<br>`--read` | Decipher a Morse Code audio file | At the prompt, enter *hi* and if `hi.wav` exists, the text *Hello World* will be displayed 

This was a fun little project and I was really happy to have [my PR merged](https://github.com/nimit2801/MorseCodeMessenger/pull/3).