Detta är ett inlägg i serien datorarkeologi.
I tidigare inlägg har jag visat hur man automatiskt kan översätta skriven text till morse och därefter generera en ljudfil som man t.ex. kunde köra igenom en radiosändare och om effekten är tillräckligt stark och reflexionerna i atmosfären optimala så kan man i princip höra morsemeddelandet överallt på jorden.
Vad gör jag om jag inte kan morse d.v.s. meddelandet är bara en okänd serie blippar. Kan jag skriva ett program som läser in morsekoden som ljud och översätter ljudmeddelandet till text? Det visar sig att detta, om signalen är optimal, är mycket enkelt att göra. Amplituden på alla ljudsignaler är exakt lika och alla pauser har någon av tre exakt definierade längder.
Signalen har följande utseende om jag tittar på ljudfilen med hjälp av programmet Audacity:

Vi ser att signalen är extremt ren och störningsfri. Pauser har ljudnivån noll helt utan något brus och signalen är en sinusvåg med konstant amplitud.
För att kunna läsa av signalen likriktar vi den först d.v.s. vi tar absolutvärdet av signalen så att alla negativa värden under strecket i figuren blir positiva. Signalen varierar våldsamt mellan 0 och ca. 0.5 och för att vi skall kunna bedöma om vi detekterar ljud eller tystnad filtrerar vi signalen så att vi beräknar medelvärdet av ett antal ljudvärden. Om medelvärdet är klart positivt så hör programmet ljud och då medelvärdet ligger mycket nära noll så är det tyst.
Programmet har en funktion/subrutin. Jag skrev denna gång programmet i språket python. Översättningen går till så att jag samlar ihop fragment av korta ljdsignaler (*) och långa ljudsignaler (-). Ljudsignalerna läggs till en textsträng ända tills vi stöter på en ”teckenpaus” d.v.s. en paus mellan bokstäver. Vi skickar nu textsträngen t.ex. ‘*-‘ till funktionen translate_char(tecken) som jämför morsetecknet med alla morsetecken i morsealfabetet och därefter skriver ut resultatet som bokstaven ‘a’ i detta fall. Vi nollar nu morsesträngen och börjar samla * rep – för följande tecken. För att läsa vad programmet gör så hoppar vi över funktionen translate_char och börjar läsa kommandoraden på vilken vi vill ha endast en parameter d.v.s. ljudfilens namn. Ljudfilen kan vara en mp3- eller en wav-fil. Om vi ger en mp3-fil så konverterar programmet automatiskt mp3 filen till en wav-fil eftersom wav-filen är lättare att hantera rent tekniskt.
#!/home/lasi/miniconda3/bin/python
# Name=morse_receiver.py
# The program reads an audio file and converts the audio back to plain text.
# The analysis works as follows:
# Read the message and record the lengths of sound and silence to a file.
# Determine the length of dots and dashes and the lengths of silence.
# Create a new file vith * - and the character interval + word interval.
# The file can now be analyzed for morse patterns and converted into text.
# This is free code. Use on your own risk.
import wavfile
import sys
import os
def translate_char(m_string):
m_string.strip()
if (m_string=="*-"):
return "A"
if (m_string=="-***"):
return "B"
if (m_string=="-*-*"):
return "C"
if (m_string=="-**"):
return "D"
if (m_string=="*"):
return "E"
if (m_string=="**-*"):
return "F"
if (m_string=="--*"):
return "G"
if (m_string=="****"):
return "H"
if (m_string=="**"):
return "I"
if (m_string=="*---"):
return "J"
if (m_string=="-*-"):
return "K"
if (m_string=="*-**"):
return "L"
if (m_string=="--"):
return "M"
if (m_string=="-*"):
return "N"
if (m_string=="---"):
return "O"
if (m_string=="*--*"):
return "P"
if (m_string=="--*-"):
return "Q"
if (m_string=="*-*"):
return "R"
if (m_string=="***"):
return "S"
if (m_string=="-"):
return "T"
if (m_string=="**-"):
return "U"
if (m_string=="***-"):
return "V"
if (m_string=="*--"):
return "W"
if (m_string=="-**-"):
return "X"
if (m_string=="-*--"):
return "Y"
if (m_string=="--**"):
return "Z"
if (m_string=="*--*-"):
return "Å"
if (m_string=="*-*-"):
return "Ä"
if (m_string=="---*"):
return "Ö"
if (m_string=="*----"):
return "1"
if (m_string=="**---"):
return "2"
if (m_string=="***--"):
return "3"
if (m_string=="****-"):
return "4"
if (m_string=="*****"):
return "5"
if (m_string=="-****"):
return "6"
if (m_string=="--***"):
return "7"
if (m_string=="---**"):
return "8"
if (m_string=="----*"):
return "9"
if (m_string=="-----"):
return "0"
# Primitive error check
print("Error m_string=",m_string)
print("Len=",len(m_string))
return "?"
if (len(sys.argv)<1) or (len(sys.argv)>=3):
print("Usage: morse_receiver.py snd_file")
exit(0)
# We only process wav-files. If I get a mp3 then convert it to wav
# Add further conversions here as nedessary.
# Filtypen bestäms utifrån filnamnet inte från magisk filtyp.
snd_file = sys.argv[1]
print("File to process: ",snd_file)
if snd_file.endswith('.mp3'):
print("MP3 file detected")
# Convert to a wav file
# model ffmpeg -i song.mp3 -ar 44100 song.wav
cmd = "ffmpeg -i "+snd_file+" -loglevel quiet -ar 44100 -y "+snd_file+".wav >/dev/null"
snd_file=snd_file+".wav"
print("Converted file="+snd_file)
# Convert the mp3 file to wav before processing.
os.system(cmd)
f = wavfile.open(snd_file, 'r')
frames=f.num_frames
wav_data=f.read_float(frames)
ampl=0
i=0
snd = False
nosnd = True
pstart=0
sstart=0
my_ch = "";
for d in wav_data:
ampl=(9*ampl+abs(d[0]))/10
i=i+1
if((ampl>0.1) and (nosnd==True)):
sstart=i
l = i-pstart
if(l<5000):
sp=0
elif((l>5000) and (l<20000)):
print(translate_char(my_ch)+" "+my_ch)
my_ch=""
else:
print(translate_char(my_ch)+" "+my_ch+"\n")
my_ch=""
snd = True
nosnd = False
elif((ampl<0.01) and (snd==True)):
pstart = i
if((i-sstart)<5000):
my_ch = my_ch + "*"
else:
my_ch = my_ch + "-"
snd=False
nosnd=True
Vi kontrollerar om vi fick en mp3-fil som parameter. Om detta är fallet så bygger vi upp ett kommando som en textsträng där programmet ffmpeg används för att göra en wav-kopia av mp3-filen. Kommandot utförs av det externa programmet ffmpeg genom att anropa det via system() d.v.s. vi gör inifrån programmet detsamma som vi skulle ha kunnat göra på kommandoraden.
if snd_file.endswith('.mp3'):
print("MP3 file detected")
# Convert to a wav file
# model ffmpeg -i song.mp3 -ar 44100 song.wav
cmd = "ffmpeg -i "+snd_file+" -loglevel quiet -ar 44100 -y "+snd_file+".wav >/dev/null"
snd_file=snd_file+".wav"
print("Converted file="+snd_file)
# Convert the mp3 file to wav before processing.
os.system(cmd)
Vi läser nu in hela ljudfilen i minnet, PDP11 skulle storkna i detta skede eftersom användarminnet skulle ta slut innan ens halva filen är läst … fint att ha lite mera minne i en modern dator! Vi skapar också några hjälpvariabler som vi behöver lite senare. Om jag skulle dekoda en fil på PDP11 så skulle jag läsa in data från skiva i stället för att ha filen i datorns minne. Att använda skiva i stället för minnet fungerar precis lika bra men hastigheten är kanske en tusendel jämfört med att jobba direkt mot minne. PDP11 från 1970-talet skulle tugga länge på en dekodning av en 20 sekunders ljudfil. Gissar någon minut.
f = wavfile.open(snd_file, 'r')
frames=f.num_frames
wav_data=f.read_float(frames)
ampl=0
i=0
snd = False
nosnd = True
pstart=0
sstart=0
my_ch = "";
Vi börjar nu läsa in värden, ett datavärde i taget från filen som alltså ligger i centralminnet (RAM) och beräknar ett flytande medelvärde över tio ljudvärden. Experiment visade att detta gav en pålitlig detektion. Jag är övertygad om att en annan filtrering skulle fungera lika bra. För en annan ljudfil genererad av en utomstående producemt så skulle vi antagligen behöve experimentera här.
for d in wav_data:
ampl=(9*ampl+abs(d[0]))/10
Vi går nu vidare och ser när vi stöter på ljud och lägger då på minnet vilket ljudvärde 0 … vi hade och kontrollerar samtidigt om vi går från noll (icke ljud) mot ljud (större än ca. 0.1). Vi kan nu skilja på en ljudpuls och en paus. Genom att vi lagrade start och slut på pulsen så kan vi genom subtraktion beräkna längden på en ljudpuls eller en paus. Om vi stötte på en kort ljudpuls så lägger vi till ‘*’ i slutet av variablen my_ch . Om vi stötte på en lång ljudpuls så lägger vi till ‘-‘. Om vi stötte på en mellanlång eller lång paus så vet vi att tecknet är färdigt för översättning. Om vi stöter på en riktigt lång paus så vet vi att ett ord har passerats och då skriver vi ett radbyte för att underlätta läsningen.
Hela detektorn har då följande utseende:
for d in wav_data:
ampl=(9*ampl+abs(d[0]))/10
i=i+1
if((ampl>0.1) and (nosnd==True)):
sstart=i
l = i-pstart
if(l<5000):
sp=0
elif((l>5000) and (l<20000)):
print(translate_char(my_ch)+" "+my_ch)
my_ch=""
else:
print(translate_char(my_ch)+" "+my_ch+"\n")
my_ch=""
snd = True
nosnd = False
elif((ampl<0.01) and (snd==True)):
pstart = i
if((i-sstart)<5000):
my_ch = my_ch + "*"
else:
my_ch = my_ch + "-"
snd=False
nosnd=True
Intresserade läsare kan lyssna på morsekoden nedan. Min reaktion på denna morsemottagare är egentligen att det visade sig vara mycket lättare att skriva mottagaren än jag hade väntat mig.
Om någon läsare vill skriva en mottagare för svårare morsekod t.ex. morsekod där alla tidsvärden varierar då en människa sänder morse så gissar jag att jag skulle spela in alla meddelanden. Därefter skulle jag skriva en dynamisk analysator som gissar längden på kort/långt ljud samt länden av pause. Dv.s. jag skulle mäta längden på alla tidsvärden separat för vatje fil.
En annan komplikation är att signalamplituden sannolikt skulle kunna variera rätt mycket. Även detta skulle kräva separat hantering så att gränsvärdet för ljud/tystnad skulle kunna väljas utgående från signalen i stället för att ges ett fast värde som i detta exemple.
09/02/2022 kl. 20:36 |
Nu fungerar mp3 filen
Den ons 9 feb. 2022 kl 16:00 skrev Larsil2009’s Blog :
> Lars Silén: Reflex och spegling posted: ” Detta är ett inlägg i serien > datorarkeologi. I tidigare inlägg har jag visat hur man automatiskt kan > översätta skriven text till morse och därefter generera en ljudfil som man > t.ex. kunde köra igenom en radiosändare och om effekten är tillräckligt > sta” >