0.0.1 commit

This commit is contained in:
Bigsk 2023-02-23 00:20:48 +08:00
parent e7c23331a3
commit a6e305a3d6
11 changed files with 357 additions and 0 deletions

8
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

4
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="C:\Users\Bigsk\anaconda3" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/pycw.iml" filepath="$PROJECT_DIR$/.idea/pycw.iml" />
</modules>
</component>
</project>

12
.idea/pycw.iml generated Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="C:\Users\Bigsk\anaconda3" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="GOOGLE" />
<option name="myDocStringFormat" value="Google" />
</component>
</module>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

20
pycw/__init__.py Normal file
View File

@ -0,0 +1,20 @@
from . import synth
from .morse import DIT, DAH, MORSE_TABLE
from .morse import generate
from .morse import stream_wave
from .morse import output_wave
from .morse import normalize_text
__version__ = '0.0.1'
__all__ = [
"synth",
"DIT",
"DAH",
"MORSE_TABLE",
"generate",
"stream_wave",
"output_wave",
"normalize_text"
]

41
pycw/__main__.py Normal file
View File

@ -0,0 +1,41 @@
from argparse import ArgumentParser
from . import output_wave
def main():
parser = ArgumentParser(
description='Generate Morse Code (CW) audio files in Python.'
)
parser.add_argument('-i', '--input', default="-",
help='Input text file (defaults to stdin)')
parser.add_argument('-t', '--text',
help='Input text. Overrides --input.')
parser.add_argument('-s', '--speed', type=int, default=12,
help='Speed, in words per minute (default: 12)')
parser.add_argument('-n', '--tone', type=int, default=800,
help='Tone frequency, in Hz (default: 800)')
parser.add_argument('-v', '--volume', type=float, default=1.0,
help='Volume (default: 1.0)')
parser.add_argument('-r', '--sample_rate', type=int, default=44100,
help='Sample rate (default: 44100)')
parser.add_argument('-o', '--output',
help='Name of the output file')
args = parser.parse_args()
if not args.input:
with open(args.input, "r") as fb:
input_text = fb.read()
else:
input_text = args.text
if input_text == "-":
print('usage: -h to learn more')
else:
output_wave(
args.output, input_text,
args.speed, args.tone, args.volume,
args.sample_rate
)
if __name__ == '__main__':
main()

151
pycw/morse.py Normal file
View File

@ -0,0 +1,151 @@
import re
import wave
import numpy
from .synth import generate_silence, generate_sin_wave
DEFAULT_SAMPLE_RATE = 44100
DEFAULT_VOLUME = 1.0
DEFAULT_TONE = 800
DIT = object()
DAH = object()
SYMBOL_SPACE = object()
LETTER_SPACE = object()
WORD_SPACE = object()
MORSE_TABLE = {
'a': (DIT, DAH),
'b': (DAH, DIT, DIT, DIT),
'c': (DAH, DIT, DAH, DIT),
'd': (DAH, DIT, DIT),
'e': (DIT, ),
'f': (DIT, DIT, DAH, DIT),
'g': (DAH, DAH, DIT),
'h': (DIT, DIT, DIT, DIT),
'i': (DIT, DIT),
'j': (DIT, DAH, DAH, DAH),
'k': (DAH, DIT, DAH),
'l': (DIT, DAH, DIT, DIT),
'm': (DAH, DAH),
'n': (DAH, DIT),
'o': (DAH, DAH, DAH),
'p': (DIT, DAH, DAH, DIT),
'q': (DAH, DAH, DIT, DAH),
'r': (DIT, DAH, DIT),
's': (DIT, DIT, DIT),
't': (DAH, ),
'u': (DIT, DIT, DAH),
'v': (DIT, DIT, DIT, DAH),
'w': (DIT, DAH, DAH),
'x': (DAH, DIT, DIT, DAH),
'y': (DAH, DIT, DAH, DAH),
'z': (DAH, DAH, DIT, DIT),
'0': (DAH, DAH, DAH, DAH, DAH),
'1': (DIT, DAH, DAH, DAH, DAH),
'2': (DIT, DIT, DAH, DAH, DAH),
'3': (DIT, DIT, DIT, DAH, DAH),
'4': (DIT, DIT, DIT, DIT, DAH),
'5': (DIT, DIT, DIT, DIT, DIT),
'6': (DAH, DIT, DIT, DIT, DIT),
'7': (DAH, DAH, DIT, DIT, DIT),
'8': (DAH, DAH, DAH, DIT, DIT),
'9': (DAH, DAH, DAH, DAH, DIT),
'.': (DIT, DAH, DIT, DAH, DIT, DAH),
',': (DAH, DAH, DIT, DIT, DAH, DAH),
'/': (DAH, DIT, DIT, DAH, DIT),
'?': (DIT, DIT, DAH, DAH, DIT, DIT),
'=': (DAH, DIT, DIT, DIT, DAH),
"'": (DIT, DAH, DAH, DAH, DAH, DIT),
'!': (DAH, DIT, DAH, DIT, DAH, DAH),
'(': (DAH, DIT, DAH, DAH, DIT),
')': (DAH, DIT, DAH, DAH, DIT, DAH),
'&': (DIT, DAH, DIT, DIT, DIT),
':': (DAH, DAH, DAH, DIT, DIT, DIT),
';': (DAH, DIT, DAH, DIT, DAH, DIT),
'+': (DIT, DAH, DIT, DAH, DIT),
'-': (DAH, DIT, DIT, DIT, DIT, DAH),
'_': (DIT, DIT, DAH, DAH, DIT, DAH),
'"': (DIT, DAH, DIT, DIT, DAH, DIT),
'$': (DIT, DIT, DIT, DAH, DIT, DIT, DAH),
}
def generate(
text: str, wpm: int, tone: int = DEFAULT_TONE,
volume: float = DEFAULT_VOLUME, sample_rate: int = DEFAULT_SAMPLE_RATE
) -> numpy.array:
text = normalize_text(text)
samples = list(_generate_samples(text, wpm, tone, volume, sample_rate))
return numpy.concatenate(samples)
def stream_wave(
fp: wave.Wave_write, text: str, wpm: int, tone: int = DEFAULT_TONE,
volume: float = DEFAULT_VOLUME, sample_rate: int = DEFAULT_SAMPLE_RATE
) -> None:
text = normalize_text(text)
for sample in _generate_samples(text, wpm, tone, volume, sample_rate):
fp.writeframes(sample)
def output_wave(
file: str, text: str, wpm: int, tone: int = DEFAULT_TONE,
volume: float = DEFAULT_VOLUME, sample_rate: int = DEFAULT_SAMPLE_RATE
) -> None:
text = normalize_text(text)
with wave.open(file, "wb") as fp:
fp.setnchannels(1)
fp.setsampwidth(2)
fp.setframerate(sample_rate)
for sample in _generate_samples(text, wpm, tone, volume, sample_rate):
fp.writeframes(sample)
def _generate_samples(
text: str, wpm: int, tone: int = DEFAULT_TONE,
volume: float = DEFAULT_VOLUME, sample_rate: int = DEFAULT_SAMPLE_RATE
) -> numpy.array:
dit_duration = 1.2 / wpm
dah_duration = dit_duration * 3
symbol_space_duration = dit_duration
letter_space_duration = (dit_duration * 3) - symbol_space_duration
word_space_duration = (dit_duration * 7) - letter_space_duration
audio_params = {
'attack': dit_duration / 10,
'release': dit_duration / 10,
'volume': volume,
'sample_rate': sample_rate
}
samples = {
DIT: generate_sin_wave(tone, dit_duration, **audio_params),
DAH: generate_sin_wave(tone, dah_duration, **audio_params),
SYMBOL_SPACE: generate_silence(symbol_space_duration),
LETTER_SPACE: generate_silence(letter_space_duration),
WORD_SPACE: generate_silence(word_space_duration),
}
def _encode_letter(letter: str):
if letter == ' ':
yield samples[WORD_SPACE]
return
symbols = MORSE_TABLE.get(letter)
if not symbols:
raise ValueError('Unsupported symbol: {}'.format(repr(letter)))
for symbol in symbols:
yield samples[symbol]
yield samples[SYMBOL_SPACE]
yield samples[LETTER_SPACE]
for letter in normalize_text(text):
yield from _encode_letter(letter)
def normalize_text(text: str) -> str:
text = re.sub(r'\s+', ' ', text.lower().strip())
return text

53
pycw/synth.py Normal file
View File

@ -0,0 +1,53 @@
import numpy
DEFAULT_SAMPLE_RATE = 44100
DEFAULT_VOLUME = 1.0
def generate_sin_wave(
frequency: int,
duration: float,
volume: float = DEFAULT_VOLUME,
attack: int = 0,
release: int = 0,
sample_rate: int = int(DEFAULT_SAMPLE_RATE)
):
samples = volume * numpy.sin(
2 * numpy.pi *
numpy.arange(sample_rate * duration) *
frequency / sample_rate,
).astype(numpy.float32)
if (attack + release) > duration:
raise ValueError('Attack + release times cannot be > total time')
if attack > 0:
attack_samples_num = int(attack * sample_rate)
attack_samples = samples[:attack_samples_num]
other_samples = samples[attack_samples_num:]
attack_envelope = (
numpy.arange(
attack_samples_num
) / attack_samples_num
)
samples = numpy.concatenate((
(attack_samples * attack_envelope), other_samples
))
if release > 0:
release_samples_num = int(release * sample_rate)
release_samples = samples[-release_samples_num:]
other_samples = samples[:-release_samples_num]
release_envelope = (
numpy.arange(
release_samples_num - 1, -1, -1
) / release_samples_num
)
samples = numpy.concatenate((
other_samples, (release_samples * release_envelope)
))
return (samples * (2**15 - 1) / numpy.max(numpy.abs(samples))).astype(numpy.int16)
def generate_silence(duration: float, sample_rate: int = DEFAULT_SAMPLE_RATE):
return numpy.zeros(int(duration * sample_rate)).astype(numpy.int16)

48
setup.py Normal file
View File

@ -0,0 +1,48 @@
from setuptools import setup, find_packages
version = '0.0.1'
longdesc = """\
PyCW
#######
Generate Morse Code (CW) audio files in Python.
Repository and documentation: https://github.com/bigsk05/pycw
"""
setup(
name='pycw',
version=version,
packages=find_packages(),
url='https://github.com/bigsk05/pycw',
license='Apache Software License',
author='Ian Xia',
author_email='i@ianxia.com',
description='Generate Morse Code (CW) audio files in Python',
long_description=longdesc,
install_requires=[
'numpy',
],
classifiers=[
'License :: OSI Approved :: Apache Software License',
# 'Programming Language :: Python :: 3',
# 'Programming Language :: Python :: 3.0',
# 'Programming Language :: Python :: 3.1',
# 'Programming Language :: Python :: 3.2',
# 'Programming Language :: Python :: 3.3',
# 'Programming Language :: Python :: 3.4',
# 'Programming Language :: Python :: 3.5',
# 'Programming Language :: Python :: 3.6',
# 'Programming Language :: Python :: 3.7',
# 'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
# 'Programming Language :: Python :: 3 :: Only',
],
entry_points={
'console_scripts': ['pycw=pycw:main'],
},
package_data={'': ['README.md']},
include_package_data=True,
zip_safe=False)