Skip to content

Commit

Permalink
Merge pull request #9 from lavajuno/wav-io
Browse files Browse the repository at this point in the history
Add .wav read/write functionality for Receiver and Transmitter
  • Loading branch information
lavajuno authored Apr 16, 2024
2 parents 32b6d33 + 489641f commit e11481a
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 42 deletions.
44 changes: 37 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# afskmodem
A software-defined Audio Frequency-Shift Keying modem designed for analog FM radios. Uses the device's default audio input and output.
A software-defined Audio Frequency-Shift Keying modem designed for analog FM
radios. Uses the device's default audio input and output.

[Source code](https://github.com/lavajuno/afskmodem)

Expand Down Expand Up @@ -34,8 +35,28 @@ from afskmodem import Receiver
r = Receiver(1200, 14000, 11000)
```

> Note: Although it is possible to change these parameters, the defaults usually perform the best.
> The input device's volume should be adjusted before changing the sensitivity of Receiver.
> Note: Although it is possible to change these parameters, the defaults usually
> perform the best.
> The input device's volume should be adjusted before changing the sensitivity
> of Receiver.
### Writing to a .wav file with Transmitter:
```python
from afskmodem import Transmitter
t = Transmitter(1200)
t.save("Hello World!".encode("ascii", "ignore"), "myfile.wav")
```

### Reading from a .wav file with Receiver:
```python
from afskmodem import Receiver
r = Receiver(1200)
recv_data = r.read("myfile.wav")
print(recv_data.decode("ascii", "ignore"))
```

> Note: Input files for Receiver.read() must be 48khz 16-bit mono.
> Transmitter.write() outputs files in this format.
# Supported Baud Rates
afskmodem supports common baud rates like 300, 600, 1200, 2400, 4800, and 9600.
Expand All @@ -52,14 +73,19 @@ and provides functionality for receiving and decoding messages.

`baud_rate`: (required, int, 300-12000) Baud rate for this Receiver

`amp_start_threshold`: (optional, int, 0-32768) Amplitude to detect start of signal
`amp_start_threshold`: (optional, int, 0-32768) Amplitude to detect start of
signal

`amp_end_threshold`: (optional, int, 0-32768) Amplitude to detect end of signal


### Member Functions

`receive(timeout: float) -> bytes` - Listens and decodes a message. Takes a timeout in seconds.
`receive(timeout: float) -> bytes` - Listens and decodes a message. Takes a
timeout in seconds.

`read(filename: str) -> bytes` - Listens and decodes a message, reading input
from the specified .wav file. Input file must be 48khz 16-bit mono.

## Transmitter

Expand All @@ -70,8 +96,12 @@ and provides functionality for encoding and sending messages.

`baud_rate`: (required, int, 300-12000) Baud rate for this Transmitter

`training_sequence_time`: (optional, float) Length of the training sequence in seconds.
`training_sequence_time`: (optional, float) Length of the training sequence in
seconds

### Member Functions

`transmit(data: bytes)`: Encodes and transmits a message
`transmit(data: bytes)`: Encodes and transmits a message.

`write(data: bytes, filename: str)`: Encodes and transmits a message, saving
the audio to a .wav file instead of playing it.
113 changes: 82 additions & 31 deletions afskmodem.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@
afskmodem.py
https://lavajuno.github.io/afskmodem
Author: Juno Meifert
Modified: 2024-04-15
"""

import pyaudio

import wave
from datetime import datetime

# Log level (0: Info (Recommended), 1: Warn, 2: Error, 3: None)
# Log level (0: Debug, 1: Info (Recommended), 2: Warn, 3: Error, 4: Fatal)
LOG_LEVEL = 0

"""
Expand All @@ -21,19 +24,18 @@ def __init__(self, class_name: str):
def __print(self, level: int, message: str):
if(level >= LOG_LEVEL):
output = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
output += " (afskmodem) "
match level:
case 1:
output += "[ INFO ] "
output += " [ INFO ] "
case 2:
output += "[ WARN ] "
output += " [ WARN ] "
case 3:
output += "[ ERROR ] "
output += " [ ERROR ] "
case 4:
output += "[ FATAL ] "
output += " [ FATAL ] "
case _:
output += "[ DEBUG ] "
output += self.__class_name.ljust(20)
output += " [ DEBUG ] "
output += self.__class_name.ljust(24)
output += ": "
output += message
print(output)
Expand Down Expand Up @@ -188,23 +190,34 @@ def __init__(self):
)

# Starts the input stream
def start(self):
def start(self) -> None:
self.__stream.start_stream()

# Stops the input stream
def stop(self):
def stop(self) -> None:
self.__stream.stop_stream()

# Listens to input stream and returns a list of frames
def listen(self) -> list[int]:
frames: bytes = self.__stream.read(2048)

# Converts frames from bytes to a list of integers
def __convertFrames(frames: bytes) -> list[int]:
res: list[int] = []
for i in range(0, len(frames) - 1, 2):
res.append(int.from_bytes(frames[i:i+2], "little", signed=True))
return res


# Listens to input stream and returns a list of frames
def listen(self) -> list[int]:
return SoundInput.__convertFrames(self.__stream.read(2048))

# Loads a wav file and returns a list of frames
# Input file must be 48khz 16-bit mono
def loadFromFile(filename: str) -> list[int]:
with wave.open(filename, 'rb') as f:
return SoundInput.__convertFrames(
f.readframes(f.getnframes())
)

# Closes the input stream
def close(self):
def close(self) -> None:
self.__stream.close()

"""
Expand All @@ -221,16 +234,34 @@ def __init__(self):
output = True
)
self.__stream.start_stream()
# Writes frames to the output stream and blocks
def play(self, frames: list[int]):
out_frames: bytearray = []

# Converts frames from a list of integers to bytes
def __convertFrames(frames: list[int]) -> bytes:
res: bytearray = []
for i in range(0, len(frames) - 1, 2):
frame = frames[i].to_bytes(2, 'little', signed=True)
out_frames.extend(frame * 2)
self.__stream.write(bytes(out_frames), len(frames),
exception_on_underflow=False)

res.extend(frame * 2)
return bytes(res)

# Writes frames to the output stream and blocks
def play(self, frames: list[int]) -> None:
self.__stream.write(
SoundOutput.__convertFrames(frames),
len(frames),
exception_on_underflow=False
)

# Writes frames to a wav file
# Output file will be 48khz 16-bit mono
def writeToFile(filename: str, frames: list[int]) -> None:
with wave.open(filename, 'wb') as f:
f.setnchannels(1)
f.setsampwidth(2)
f.setframerate(48000)
f.writeframes(
SoundOutput.__convertFrames(frames)
)

# Closes the output stream
def close(self):
self.__stream.stop_stream()
Expand All @@ -250,7 +281,7 @@ def __init__(self, baud_rate: int = 1200, amp_start_threshold: int = 18000,
self.__mark_tone: list[int] = Waveforms.getMarkTone(baud_rate)
self.__training_cycle: list[int] = Waveforms.getTrainingCycle(baud_rate)
self.__sound_in: SoundInput = SoundInput()
self.__log = Log("Receiver")
self.__log = Log("afskmodem.Receiver")

# Amplifies a received signal
def __amplify(self, chunk: list[int]) -> list[int]:
Expand Down Expand Up @@ -367,7 +398,7 @@ def __bitsToBytes(self, bits: str) -> bytes:
i += 8
return bytes(res)

# Receives data and returns it (or fails)
# Receives signal, decodes it, then returns it (or fails)
def receive(self, timeout: float) -> bytes:
self.__log.info("Listening...")
recv_audio = self.__listen(int(timeout * 48000))
Expand All @@ -382,6 +413,18 @@ def receive(self, timeout: float) -> bytes:
dec_bytes = self.__bitsToBytes(dec_bits)
self.__log.debug("Decoded " + str(len(dec_bytes)) + " bytes.")
return dec_bytes

# Reads signal from a file, decodes it, then returns it (or fails)
def read(self, filename: str) -> bytes:
recv_bits = self.__decodeBits(SoundInput.loadFromFile(filename))
if(recv_bits == ""):
self.__log.warn("No data.")
return b""
dec_bits = ECC.decode(recv_bits)
dec_bytes = self.__bitsToBytes(dec_bits)
self.__log.debug("Decoded " + str(len(dec_bytes)) + " bytes.")
return dec_bytes


"""
Transmitter manages a line to the default audio output device
Expand All @@ -394,18 +437,16 @@ def __init__(self, baud_rate: int = 1200, training_time: float = 0.5):
self.__mark_tone = Waveforms.getMarkTone(baud_rate)
self.__training_cycle = Waveforms.getTrainingCycle(baud_rate)
self.__sound_out = SoundOutput()
self.__log = Log("Transmitter")
self.__log = Log("afskmodem.Transmitter")

# Convert bytes to bits
def __bytesToBits(self, b_in: bytes) -> str:
bits = ""
for i in range(len(b_in)):
bits += '{0:08b}'.format(b_in[i])
return bits

# Transmits the given data.
def transmit(self, data: bytes):
self.__log.info("Transmitting " + str(len(data)) + " bytes...")

def __getFrames(self, data: bytes) -> bytes:
frames: list[int] = []
message_bits = self.__bytesToBits(data)
ecc_bits = ECC.encode(message_bits)
Expand All @@ -422,5 +463,15 @@ def transmit(self, data: bytes):
else:
frames.extend(self.__mark_tone)
frames.extend([0] * 4800)
return frames

# Transmits the given data.
def transmit(self, data: bytes):
self.__log.info("Transmitting " + str(len(data)) + " bytes...")
frames: bytes = self.__getFrames(data)
self.__log.info("Transmitting " + str(len(frames)) + " frames...")
self.__sound_out.play(frames)

# Transmits the given data, saving the resulting audio to a .wav file.
def write(self, data: bytes, filename: str):
SoundOutput.writeToFile(filename, self.__getFrames(data))
24 changes: 24 additions & 0 deletions rx-demo-file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from afskmodem import Receiver

def main():
receiver = Receiver(1200)
print("AFSKmodem File Read Demo")
while(True):
print("Enter path to file to read (ex. \"./myfile.wav\"):")
rxData = receiver.read(input())
if(rxData == b""):
print("Could not decode.")
else:
print("Transmission decoded:")
print("")
print(rxData.decode("utf-8", "ignore"))
print("")
print("Done. (CTRL-C to exit)")


if(__name__ == "__main__"):
try:
main()
except(KeyboardInterrupt):
print("CTRL-C")
pass
8 changes: 5 additions & 3 deletions rx-demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ def main():
receiver = Receiver(1200)
print("AFSKmodem RX Demo")
while(True):
print("Waiting for message...\n")
print("Waiting for message...")
while(True):
rxData = receiver.receive(100)
if(rxData != b""):
print("Transmission received.")
print("Transmission received:")
print("")
print(rxData.decode("utf-8", "ignore"))
print("\nDone. (CTRL-C to exit)")
print("")
print("Done. (CTRL-C to exit)")
break

if(__name__ == "__main__"):
Expand Down
21 changes: 21 additions & 0 deletions tx-demo-file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from afskmodem import Transmitter

def main():
transmitter = Transmitter(1200)
print("AFSKmodem File Write Demo")
while True:
print("Enter message string (ASCII):")
userMessage = input()
print("Enter path to save file at (ex. \"./myfile.wav\"):")
filename = input()
print("Saving to file...")
transmitter.write(userMessage.encode("ascii", "ignore"), filename)
print("Done. (CTRL-C to exit)")
print("")

if(__name__ == "__main__"):
try:
main()
except(KeyboardInterrupt):
print("CTRL-C")
pass
3 changes: 2 additions & 1 deletion tx-demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ def main():
userMessage = input()
print("Transmitting...")
transmitter.transmit(userMessage.encode("ascii", "ignore"))
print("Done. (CTRL-C to exit)\n")
print("Done. (CTRL-C to exit)")
print("")

if(__name__ == "__main__"):
try:
Expand Down

0 comments on commit e11481a

Please sign in to comment.