과제 1: Morse code WAV file 만들기

목표

  • “CNU CSE 202302627 CHOISOYEON” 텍스트를 WAV 파일로 변환하기

과정

  • 텍스트를 먼저 모스 부호로 변환하고, 변환된 모스 부호를 WAV파일로 변환하고자 하였다.
  • string(텍스트)에 text2morse 함수를 적용하여 모스 부호를 만들어내고, 만든 모스부호에 morse2audio 함수를 적용하여 오디오를 생성하고, audio2file로 최종 WAV 파일을 생성했다.

1. 라이브러리 및 mapping dictionary

  • math: 사인파 생성(sin, pi 사용)을 위해 필요
  • wave: WAV 파일 생성 및 저장을 위해 필요
  • struct: audio data를 binary 형식으로 packing하기 위해 필요
  • mapping dictionary: 영어 알파벳(A-Z)과 숫자(0-9)에 모스부호가 각각 key, value로 대응되도록 했다.

2. text2morse

  • text2morse는 텍스트를 모스 부호로 변환해주는 함수로, 모스부호(string)를 반환한다.
  • 모스부호는 대소문자를 구분하지 않고, mapping dictionary에도 영어 대문자만 정의해두었으므로 text에 소문자가 들어있을 경우를 대비해 .upper()를 통해 모두 대문자로 바꿔준다 (이번 과제의 경우 굳이 사용하지 않아도 됐다)
  • 모스부호 변환 결과를 담을 변수 morse를 초기하하고, 텍스트를 한 글자씩 돌며 문자(key)를 모스부호(value)로 변환하여 morse에 붙인다. 이때, 모스부호의 공백은 ‘/’로 표시하므로 공백을 만나면 ‘/’로 변환한다.
  • for문을 돌며 모든 텍스트의 변한이 끝나면 morse를 반환한다.

3. morse2audio

  • morse2audio는 모스 부호를 오디오 신호로 변환하는 함수이다.

  • 상수 (*과제 조건)
  • for문을 돌며 모스 부호 해석

4. audio2file

  • wave.open으로 WAV 파일 생성 (쓰기 모드: ’wb’)
  • 과제 조건에 맞도록 WAV 파일 속성 설정: mono 채널, 32bit(4bytes) sample, 48,000Hz sample rate
  • struct.pack으로 각 샘플을 little-endian 32비트 정수(<l)로 변환 후 파일에 기록

결과

  • 앞서 정의한 함수들을 순차적으로 적용하여 텍스트 → 모스부호 → WAV 파일 생성에 성공했다.

전체 코드

# asgn1.py

import math
import wave
import struct

english = {
    'A': '.-',   'B': '-...', 'C': '-.-.', 'D': '-..',  'E': '.',    'F': '..-.',
    'G': '--.',  'H': '....', 'I': '..',   'J': '.---', 'K': '-.-',  'L': '.-..',
    'M': '--',   'N': '-.',   'O': '---',  'P': '.--.', 'Q': '--.-', 'R': '.-.',
    'S': '...',  'T': '-',    'U': '..-',  'V': '...-', 'W': '.--',  'X': '-..-',
    'Y': '-.--', 'Z': '--..'
}

number = {
    '1': '.----', '2': '..---', '3': '...--', '4': '....-', '5': '.....',
    '6': '-....', '7': '--...', '8': '---..', '9': '----.', '0': '-----'
}


def text2morse(text):
    text = text.upper()
    morse = ''
    for t in text:
        for key, value in english.items():
            if t == key:
                morse = morse + value + ' '
        for key, value in number.items():
            if t == key:
                morse = morse + value + ' '

        # morse 중간에 띄어쓰기 추가
        if t==' ':
            morse = morse + '/'

    return morse


def morse2audio(morse):
    INTMAX = 2**(32-1)-1
    t = 0.1  # 100ms 0.1초로 수정
    fs = 48000
    f = 523.251  # 주파수: C5
    audio = []
    
    for i, m in enumerate(morse):
        if m == '.':
            for j in range(int(t * fs * 1)):  # Dit: 1단위
                audio.append(int(INTMAX * math.sin(2 * math.pi * f * (j / fs))))
        elif m == '-':
            for j in range(int(t * fs * 3)):  # Dah: 3단위
                audio.append(int(INTMAX * math.sin(2 * math.pi * f * (j / fs))))
        elif m == '/':
            for j in range(int(t * fs * 7)):  # 단어 간 간격: 7단위
                audio.append(int(0))
        if m in ['.', '-']:
            for j in range(int(t * fs * (1 if i + 1 < len(morse) and morse[i + 1] in ['.', '-'] else 3))):
                audio.append(int(0))
    
    return audio


def audio2file(audio, filename):
    with wave.open(filename, 'wb') as w:
        w.setnchannels(1)
        w.setsampwidth(4)
        w.setframerate(48000)
        for a in audio:
            w.writeframes(struct.pack('<l', a))


resMorse = text2morse("CNU CSE 202302627 CHOISOYEON")
print(resMorse)

resWav = morse2audio(resMorse)
audio2file(resWav, "202302627_최소연.wav")

과제 2: Morse code WAV file 해석하기

목표

주어진 WAV파일에서 텍스트를 추출하기

과정

과제1에서 했던 것의 반대 과정을 수행하면 되겠다. (WAV → 모스 부호 → 텍스트)

1. 라이브러리 및 딕셔너리

  • wave: WAV 파일 생성 및 저장을 위해 필요
  • struct: audio data를 binary 형식으로 packing하기 위해 필요
  • mapping dictionary: 영어 알파벳(A-Z)과 숫자(0-9)에 모스부호가 각각 key, value로 대응되도록 했다.

2. file2audio

  • file2audio는 WAV파일에서 오디오 데이터를 읽어와 리스트로 변환한다.
  • ‘rb’ 옵션으로 WAV파일을 binary 읽기 모드로 열고, 파일의 총 프레임 수(frame)와 초당 샘플 수(framerate)를 가져온다.
  • 각 프레임을 1개씩 읽어 struct.unpack(‘<i’, frame)로 32비트 정수로 변환 후 리스트에 저장한다.
  • 이렇게 만든 오디오 샘플 값 리스트(정수 저장) audio와 framerate를 반환한다.

3. audio2morse

  • audio2morse는 오디오 데이터를 분석하여 모스부호로 변환한다.
  • 여기서 threshold는 소리와 침묵을 구분하는 임계값이다. INTMAX는 가능한 최대 진폭을 나타내며 상대적으로 약한 신호도 감지할 수 있게끔 특정 값을 곱하여 진폭에 비례하는 임계값을 만들 것이다. 이 값을 1부터 1/10씩 순차적으로 작게 설정해봤고, 작을수록 분석 결과가 정밀해짐을 확인했다(결과 부분 참고).
  • 오디오데이터 샘플을 처음부터 끝까지 하나씩 확인하며, 오디오 샘플 값의 절댓값을 비교한다. 모스부호는 소리의 존재 여부와 길이를 기준으로 판단하므로, 소리가 진동하는 방향과 상관없이 크기만 보면 된다.

3. morse2text

  • morse2text는 모스부호를 받아 알파벳과 숫자 문자열로 변환해준다.
  • 단어 사이 공백으로 약속된 ‘/’을 기준으로 문장을 단어들로 split하고, 단어를 for문으로 하나씩 처리한다. 단어 내에서 공백(’ ‘)을 기준으로 모스부호 문자를 분리하여 (strip사용으로 불필요 공백 제거) 모스부호를 하나씩 확인하고 원래의 문자와 매핑하여 text에 붙인다.

    4. main

  • 사이버캠퍼스에 배포된 WAV파일을 읽고 이를 모스부호로 변환하고, 다시 이를 텍스트로 변환하며 각 과정의 결과물을 출력하는 코드이다. 최종 텍스트는 utf-8로 인코딩하여 과제에 제출할 txt파일로 저장되게 했다.

결과

WAV file에서 추출한 문장을 txt파일에 저장되게 했고, 내용을 출력하게 했다.

여기서 아까 언급한 threshold 계산에서 INTMAX에 곱하는 값을 점점 작게 변화시켜봤더니, 잘 나오지 않던 문장이 정확히 나오는 것을 확인할 수 있었다.

분석 결과 음성 속에 있던 문장은 “HE WAS GOING IN THE FICTION DEPARTMENT”이다.

최종 코드

# asgn2.py

import wave
import struct

english = {
    'A': '.-', 'B': '-...', 'C': '-.-.', 'D': '-..', 'E': '.', 'F': '..-.',
    'G': '--.', 'H': '....', 'I': '..', 'J': '.---', 'K': '-.-', 'L': '.-..',
    'M': '--', 'N': '-.', 'O': '---', 'P': '.--.', 'Q': '--.-', 'R': '.-.',
    'S': '...', 'T': '-', 'U': '..-', 'V': '...-', 'W': '.--', 'X': '-..-',
    'Y': '-.--', 'Z': '--..'
}

number = {
    '1': '.----', '2': '..---', '3': '...--', '4': '....-', '5': '.....',
    '6': '-....', '7': '--...', '8': '---..', '9': '----.', '0': '-----'
}

def file2audio(filename):
    with wave.open(filename, 'rb') as w:
        frames = w.getnframes()
        framerate = w.getframerate()
        audio = []
        for i in range(frames):
            frame = w.readframes(1)
            audio.append(struct.unpack('<i', frame)[0])
        return audio, framerate

def audio2morse(audio, framerate):
    INTMAX = 2**(32-1)-1
    threshold = INTMAX * 0.0000001
    morse = ''
    i = 0
    while i < len(audio):
        if abs(audio[i]) > threshold:
            start = i
            while i < len(audio) and abs(audio[i]) > threshold:
                i += 1
            duration = (i - start) / framerate
            if 0.09 <= duration <= 0.11:
                morse += '.'
            elif 0.29 <= duration <= 0.31:
                morse += '-'
        else:
            start = i
            while i < len(audio) and abs(audio[i]) <= threshold:
                i += 1
            silence_duration = (i - start) / framerate
            if 0.09 <= silence_duration <= 0.11:
                morse += ''
            elif 0.29 <= silence_duration <= 0.31:
                morse += ' '
            elif silence_duration >= 0.69:
                morse += ' / '
        i += 1
    return morse.strip()

def morse2text(morse):
    morse_words = morse.split('/')
    text = ''
    for word in morse_words:
        morse_chars = word.strip().split(' ')
        for char in morse_chars:
            for key, value in english.items():
                if value == char:
                    text += key
            for key, value in number.items():
                if value == char:
                    text += key
        text += ' '
    return text.strip()

audio_data, framerate = file2audio("output_202302627_최소연.wav")
extracted_morse = audio2morse(audio_data, framerate)
print("Extracted Morse:", extracted_morse)
extracted_text = morse2text(extracted_morse)
print("Extracted Text:", extracted_text)

with open("202302627_최소연.txt", "w", encoding="utf-8") as f:
    f.write(extracted_text)

Leave a comment