PySimpleGUI

モールス信号受信練習機 ver 4.2



import PySimpleGUI as sg
import pygame
import numpy as np
import time
import random
import threading
import sys

sg.theme('LightBrown2')

# Initialize pygame mixer for sound generation (モノラル用にチャンネル数を1に設定)
pygame.mixer.init(frequency=44100, size=-16, channels=1)

# Morse code dictionary
MORSE_CODE_DICT = {
    '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': '--..',
    '1': '.----', '2': '..---', '3': '...--', '4': '....-', '5': '.....',
    '6': '-....', '7': '--...', '8': '---..', '9': '----.', '0': '-----',
}

# 単語リスト
word_list = [
    "kind", "surround", "university", "break", "it", "speech", "parachute",
    "change", "command", "refugee", "hurt", "power", "minister", "riot",
    "recover", "male", "funeral", "stop", "define", "transport", "realistic",
    "mass", "universe", "street", "he", "square", "deaf", "stretch", "organize",
    "neutral", "concern", "emotion", "corruption", "particle", "area", "station",
    "through", "get", "inform", "tell", "transportation", "fit", "limit", "tissue",
    "genocide", "old", "repeat", "dismiss", "today", "baby", "near", "television",
    "fetus", "memorial", "tube", "self", "minor", "secret", "anniversary", "spring",
    "earth", "class", "exercise", "desert", "modern", "ask", "where", "wave", "once",
    "search", "provide", "half", "grass", "organ", "fat", "but", "supervise", "morning",
    "sympathy", "with", "incite", "flower", "execute", "remove", "poverty", "year",
    "industry", "close", "rebel", "drink", "liquid", "probably", "balloon", "genes",
    "glass", "plot", "trick", "fly", "fine", "general", "wild", "adult", "sugar",
    "victim", "so", "turn", "ice", "false", "crush", "expert", "available", "ceasefire",
    "most", "when", "exchange", "overthrow", "Congress", "holy", "stone", "describe",
    "torture", "tradition", "destroy", "react", "affect", "develop", "seize", "few",
    "mother", "degree", "family", "holiday", "atmosphere", "by", "pipe", "aggression",
    "hurry", "missing", "line", "evaporate", "resolution", "local", "nerve", "temperature",
    "substance", "include", "knife", "truce", "raise", "vote", "yet", "research", "chromosome",
    "help", "surprise", "ever", "discuss", "revolt", "intervene", "on", "soon", "fact",
    "sheep", "skill", "shape", "play", "invite", "soil", "attention", "new", "happy",
    "ecology", "announce", "voice", "possible", "awake", "money", "burn", "place",
    "examine", "come", "resist", "purchase", "huge", "apologize", "insane", "struggle",
    "long", "parliament", "list", "here", "south", "summer", "tie", "partner", "brief",
    "store", "mental", "find", "thick"
]

is_playing = False  # 発声中の状態を管理するフラグ

# Function to generate sound samples for a dot or dash
# クリックノイズ対策したcode
def generate_tone(frequency, duration):
    sample_rate = 44100
    t = np.linspace(0, duration, int(sample_rate * duration), False)
    # 音波の生成
    tone = 32767 * np.sin(2 * np.pi * frequency * t)
    
    # フェードイン・フェードアウトの適用
    fade_in_out_duration = int(sample_rate * 0.01)  # 0.01秒のフェード
    fade_in = np.linspace(0, 1, fade_in_out_duration)
    fade_out = np.linspace(1, 0, fade_in_out_duration)
    
    # 音の全体にフェードイン・フェードアウトを適用
    tone[:fade_in_out_duration] *= fade_in
    tone[-fade_in_out_duration:] *= fade_out
    
    sound_array = tone.astype(np.int16)
    
    # 1次元配列を2次元に変換 (pygameのサウンド生成用に必要)
    sound_array_stereo = np.column_stack((sound_array, sound_array))
    
    return pygame.sndarray.make_sound(sound_array_stereo)



""" クリックノイズ出ているcode
def generate_tone(frequency, duration):
    sample_rate = 44100
    t = np.linspace(0, duration, int(sample_rate * duration), False)
    tone = 32767 * np.sin(2 * np.pi * frequency * t)
    sound_array = tone.astype(np.int16)
    
    # 1次元配列を2次元に変換 (pygameのサウンド生成用に必要)
    sound_array_stereo = np.column_stack((sound_array, sound_array))
    
    return pygame.sndarray.make_sound(sound_array_stereo)
"""

# Function to play a dot sound
def play_dot(frequency):
    dot_sound = generate_tone(frequency, DOT_DURATION)
    dot_sound.play()
    time.sleep(DOT_DURATION)

# Function to play a dash sound
def play_dash(frequency):
    dash_sound = generate_tone(frequency, DASH_DURATION)
    dash_sound.play()
    time.sleep(DASH_DURATION)

# Function to calculate Morse code timings based on WPM
def calculate_timings(wpm):
    unit_time = 1.2 / wpm
    dot_duration = unit_time
    dash_duration = 3 * unit_time
    symbol_space_duration = unit_time
    letter_space_duration = 3 * unit_time
    word_space_duration = 7 * unit_time
    return dot_duration, dash_duration, symbol_space_duration, letter_space_duration, word_space_duration

# Function to play Morse code for a given letter
def play_morse_code_for_letter(letter, frequency):
    morse_code = MORSE_CODE_DICT.get(letter.upper(), '')
    for symbol in morse_code:
        if not is_playing:
            break
        if symbol == '.':
            play_dot(frequency)
        elif symbol == '-':
            play_dash(frequency)
        time.sleep(SYMBOL_SPACE_DURATION)
    time.sleep(LETTER_SPACE_DURATION)

# Function to play Morse code for a word
def play_morse_code_for_word(word, frequency):
    global is_playing
    for letter in word:
        if not is_playing:
            break
        play_morse_code_for_letter(letter, frequency)
    time.sleep(WORD_SPACE_DURATION)

# Function to handle playing of Morse code for all words
def play_morse_code(word_sample, frequency):
    global is_playing
    is_playing = True
    for word in word_sample:
        if not is_playing:
            break
        play_morse_code_for_word(word, frequency)
        morse_code = ' '.join([MORSE_CODE_DICT.get(letter.upper(), '') for letter in word])
        window['SENT_OUTPUT'].update(morse_code + '\n', append=True)

# GUIレイアウト
layout = [
    [
        sg.Frame('英単語', [
            [sg.Multiline(size=(30, 30), text_color='Blue', font=('Arial', 14, 'bold'), key='INPUT')]
        ], size=(200, 450)),
        sg.Frame('発声済モールス符号', [
            [sg.Multiline(size=(80, 30), text_color='Red', font=('Arial', 14, 'bold'), key='SENT_OUTPUT')]
        ], size=(300, 450))
    ],
    [sg.Text('単語数:'), sg.Slider(range=(10, 200), default_value=10, resolution=10, orientation='h', key='NBR')],
    [sg.Text('速度(Word/m):'), sg.Slider(range=(10, 30), default_value=20, resolution=2, orientation='h', key='WPM')],
    [sg.Text('周波数(Hz):'), sg.Slider(range=(300, 1000), default_value=600, resolution=10, orientation='h', key='FREQ')],
    [sg.Text()],
    [sg.Text('                 '), sg.Button('Start'), sg.Button('Stop', disabled=True), sg.Button('All Clear'), sg.Button('End')],
    [sg.Text('JA1FML ver 4.2', text_color='LightBlue',  justification='right', expand_x=True)]
]

window = sg.Window('モールス信号練習機 ver 4.2', layout, size=(500, 700), finalize=True)

# Main event loop
while True:
    event, values = window.read()
    """
    if event == sg.WIN_CLOSED or event == 'End':
        if sg.popup_yes_no("終了しますか?") == 'Yes':
            is_playing = False
            break
    """

    if event == sg.WIN_CLOSED or event == 'End':
        if sg.popup_yes_no("終了しますか?") == 'Yes':
            window.close()
            sys.exit()  # プログラム全体を終了 break
    

    if event == 'Start':
        nbr = int(values['NBR'])
        wpm = int(values['WPM'])
        frequency = int(values['FREQ'])
        
        # 再生する単語の選択
        word_sample = random.sample(word_list, nbr)

        # Morseコードのタイミングを計算
        DOT_DURATION, DASH_DURATION, SYMBOL_SPACE_DURATION, LETTER_SPACE_DURATION, WORD_SPACE_DURATION = calculate_timings(wpm)

        # GUIに単語を表示
        word_display = '\n'.join(word_sample)
        window['INPUT'].update(word_display)
        window['SENT_OUTPUT'].update('')
        window['Start'].update(disabled=True)
        window['Stop'].update(disabled=False)
        window['All Clear'].update(disabled=True)
        
        # Start a new thread to play Morse code without blocking the GUI
        threading.Thread(target=play_morse_code, args=(word_sample, frequency), daemon=True).start()

    if event == 'Stop':
        is_playing = False
        window['Start'].update(disabled=False)
        window['Stop'].update(disabled=True)
        window['All Clear'].update(disabled=False)

    if event == 'All Clear':
        if sg.popup_yes_no("クリアしますか?") == 'Yes':
            window['INPUT'].update('')
            window['SENT_OUTPUT'].update('')
            is_playing = False

# Clean up pygame mixer resources
pygame.mixer.quit()
window.close()

PAGE TOP