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