0.0.1 commit
This commit is contained in:
parent
e7c23331a3
commit
a6e305a3d6
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# 默认忽略的文件
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# 基于编辑器的 HTTP 客户端请求
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal 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
4
.idea/misc.xml
generated
Normal 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
8
.idea/modules.xml
generated
Normal 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
12
.idea/pycw.iml
generated
Normal 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
6
.idea/vcs.xml
generated
Normal 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
20
pycw/__init__.py
Normal 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
41
pycw/__main__.py
Normal 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
151
pycw/morse.py
Normal 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
53
pycw/synth.py
Normal 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
48
setup.py
Normal 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)
|
Loading…
x
Reference in New Issue
Block a user