Institute of Computer Science
  1. Courses
  2. 2025/26 fall
  3. Computer Programming (LTAT.03.001)
ET
Log in

Computer Programming 2025/26 fall

  • Üldinfo
  • 1. Muutuja ja avaldis
  • 2. Tingimuslause
  • 3. Funktsioon
  • 4. Korduslause
  • 5. Sõned. Lihtsam failitöötlus
  • 6. Kontrolltöö 1
  • 7. Järjend
  • 8. Järjend 2
  • 9. Kahekordne tsükkel. Failitöötlus
  • 10. Andmestruktuurid
  • 11. Andmestruktuurid 2
  • 12. Kontrolltöö 2
  • 13. Objektorienteeritud programmeerimine
  • 14. Objektorienteeritud programmeerimine 2
14.1 Objektorienteeritud programmeerimine 2
14.2 Kodutöö
14.3 Harjutused
14.4 Silmaring: helitöötlus ja -süntees
  • 15. Rekursioon
  • 16. Kordamine. Projektide esitlused
  • Viiteid
  • Silmaringimaterjalid
  • Materjalid

Helitöötlus ja -süntees

Peatükk annab ülevaate heli olemusest ning vahenditest, millega saab Pythonis heliga ümber käia. Kõigepealt tutvustatakse seda, kuidas heli matemaatilisel kujul väljendada ja kuidas seda digitaalsel kujul arvutis salvestatakse. Seejärel näidatakse, kuidas Pythoni teekide abil on võimalik heli sünteesida, esitada, lindistada, töödelda ja salvestada.

Ettevalmistus

Õpikust peab olema läbitud järjendite peatükk. Soovitatav on tutvuda objektorienteeritud programmeerimise mõistetega, aga suur osa materjalist peaks olema ilma selletagi mõistetav. Helilainete kuvamiseks kasutame teeki matplotlib. Heli esitamiseks peab paigaldama teegi simpleaudio, reaalajas lindistamiseks ja esitamiseks teegi sounddevice ning töötlemiseks teegi pydub. Kõik saab näiteks käsurealt paigaldada ühe käsuga pip install matplotlib sounddevice simpleaudio pydub. Vajadusel loe läbi õpikus moodulite paigaldamise juhised.

Helilained ja -sagedused

Heli all mõistetakse seda, kui õhus edasikanduvad võnked jõuavad kõrva. Helilained on mudeldatavad siinusfunktsiooni abil, sest see kirjeldab üsna hästi millegi võnkumist. Laine amplituud A määrab sealjuures heli valjuse ning sagedus f helikõrguse. Kuna just võnkumise protsess tekitab helikogemuse, siis vaatame ühe ajahetke asemel ajavahemikku. Selle jaoks on t valemis ajavektor, mille pikkus määrab ka heli kestvuse.

{$ y = A \cdot \sin(2 \pi f \cdot t) $}

Kujutada saame seda Pythonis mooduli matplotlib abil. Kui teha joonis täppidega, siis on paremini näha, et tegemist on siiski digitaalse signaali, mitte pideva analoogsignaaliga. Samas eristub siiski laine kuju.

from math import *
import matplotlib.pyplot as plt

A = 1.0
f = 432
samplerate = 44100

t = [i/samplerate for i in range(samplerate)] # ajatelg

y = [A*sin(2*pi*f * punkt) for punkt in t] # siinusfunktsiooni telg

plt.plot(t[:700], y[:700], ".") # Teeb täppidega teljestiku
plt.show()

Katseta erinevate A, f ja t väärtustega ning uuri, kuidas saadud graafik muutub.



Kuidas käitub kõlar? Allikas: Media College

Helifailid ja parameetrid

Analooghelisignaali digitaliseerimine on sisuliselt pideva helisignaali salvestamine diskreetsete väärtuste jadana. Diskreetimissagedus (ingl sample rate) määrab väärtuste või mõõtmiste (öeldakse ka diskreetide või sämplite) arvu, mis salvestatakse ühe sekundi jooksul. Vahel (eriti videote puhul) kasutatakse ka mõistet kaadrisagedus (ingl frame rate). Mida rohkem väärtusi ajaühikus salvestada, seda täpsem on heli digitaalne esitus. Standardiks loetakse 44100 väärtust sekundis ehk 44100Hz (44.1kHz).

Salvestatud heli täpsust määrab ka bitisügavus (ingl bit depth) ehk mitut bitti kasutatakse ühe mõõtmise kohta info salvestamiseks. Vahel kasutatakse ka mõistet sämpli laius (ingl sample width), mida tavaliselt väljendatakse baitides (1 bait = 8 bitti).

Helikanalite arv (ingl channels) näitab helifailide puhul, mitme kanali kaudu heli edastada: 1 kanal = monoheli, 2 kanalit = stereo, 3+ kanalit = ruumiline heli.

Failivormingud

Heli loomise või töötlemise ajal soovitatakse kasutada vähemalt 24-bitise bitisügavuse ja 48kHz diskreetimissagedusega pakkimata helifaile, et säiliks võimalikult palju infot. Selleks otstarbeks kasutatakse tihti WAV-faile - neid on ka lihtne lugeda ja töödelda, sest andmeid hoitakse failis võrdse pikkusega lõikudena. Apple'i arvutitel kasutatakse ka AIFF-vormingut.

Heli lõplikuks salvestamiseks sobib vorming, mis kasutab kadudega pakkimist, sest see võtab vähem ruumi. Üldjuhul eemaldab kadudega vorming need helid, mida keskmine inimene niikuinii ei eristaks. Sellised vormingud on näiteks OGG, MP3 ja selle edasiarendus AAC.

Kadudeta pakkimist kasutavad vormingud FLAC ja ALAC (Apple Lossless).

MIDI-vorming

MIDI (Musical Instrument Digital Interface) on protokoll, mis kirjeldab, kuidas arvutid, süntesaatorid jm digitaalsed muusikariistad saavad omavahel suhelda. Kui eelnevalt mainitud vormingud salvestasid heli üht tüüpi väärtuste jadana ja kindla diskreetimissagedusega, siis MIDI-vormingus failid salvestavad instruktsioone. Need ütlevad, millist nooti, millise tugevusega, kui kaua ning läbi millise kanali mängida. Igale kanalile vastab üks virtuaalne instrument, nii et korraga saab mängida näiteks klaverile ja kitarrile iseloomulikke helisid.

Failide avamine ja salvestamine

Siinsetes näidetes kasutame WAV-faile, aga tasub teada, kuidas Pythoniga muudes vormingutes faile avada. Pydub suudab avada erinevaid vorminguid (v.a. MIDI), aga selle jaoks tuleb arvutisse paigaldada ka FFmpeg raamistik: FFmpegi kodulehelt allalaetud ZIP-fail tuleb lahti pakkida ning seejärel tõsta kaustast bin kõik failid oma programmi kausta. Teine võimalus on määrata bin kaust oma süsteemi keskkonnamuutujate hulka.

Kui kõik vajalik on olemas, saab teegiga pydub faile näiteks ühest vormingust teise ümber salvestada:

from pydub import AudioSegment

sound = AudioSegment.from_file('fail.mp3', format='mp3')
sound.export('fail.wav', format='wav')

Kui on vaja ainult heli taasesitada, siis piisab teegist simpleaudio või pygame. Viimane suudab muuhulgas avada ja esitada ka MIDI-faile:

import pygame

pygame.init()
pygame.mixer.music.load("fail.mid")
pygame.mixer.music.play()

MIDI-failide töötlemiseks on muud teegid, näiteks mido.

Lainevormid

Lisaks sagedusele ja amplituudile mõjutab heli kõla ka selle lainevorm. Peale siinuslaine eristatakse ka teisi lainevorme, kuigi tegelikult on needki väljendatavad erinevate siinuslainete summana. Teisisõnu, kõiki lainevorme peale siinuslaine võib põhimõtteliselt nimetada akordideks. Tabelis on toodud tuntuimad lainevormid:

LainevormValemKujuKõla (220Hz) Allikas: Wikimedia Commons
Siinuslaine{$ f(x) = \sin(2 \pi*x) $}∿
Kandiline ehk impulsslaine{$ f(x) = 2*(2*\lfloor x \rfloor - \lfloor{2*x}\rfloor) + 1 $}⎍⎍⎍
Kolmnurklaine{$ f(x) = 2*|(2x - 0.5) \bmod 2 - 1| - 1 $}/\/\/
Saehammaslaine{$ f(x) = -(2x \bmod 2) + 1 $} või

Näiteks varajaste arvutimängude helides kasutati palju kandilisi laineid. Nimelt kettaruumi oli vähe, aga sellise laine ühe punkti salvestamiseks piisab ainult ühest bitist, mis ütleb, kummas amplituudväärtuses laine parajasti on.

Tasub mainida, et väga kõrgetel sagedustel on kõik lainevormid sarnase kõla ja välimusega, sest üleminekud võngetes on iga vormi puhul väga järsud. Samas võib kuulates märgata, et näiteks impulsslained on palju valjemad kui sama amplituudiga siinuslained. Valjus oleneb sellest, kui kaua laine igal sammul oma amplituudväärtuste läheduses viibib. Siinuslained on kõige vähem aega amplituudväärtuse lähedal, aga impulsslained on pidevalt amplituudväärtuses.

Lainevormidega heli sünteesimine

Eelneva põhjal on üsna lihtne heli digitaalselt luua. Tuleb teha järjend, kus iga element on ühele ajahetkele vastav lainefunktsiooni väärtus. Esimeses koodinäites uurisime helilainet sagedusega 432Hz. Paneme nüüd näiteks kokku ühe oktavi pikkuse heliredeli.

Selleks peame teadma iga noodi võnkesagedust. Vanasti polnud ühist instrumentide häälestamise standardit ja iga helilooja otsustas ise, millisele võnkesagedusele ta oma pillid häälestas. Aja jooksul ja piirkonniti on muutunud ka noodiskaalad. Tänapäeval on läänemaailmas standardiks 12 tooniga oktavite süsteem. Põhitooni ehk viienda oktavi noodi A võnkesageduseks on valitud 440Hz. Teades põhitooni sagedust on näiteks klaveril võimalik järgmise valemiga leida ülejäänud klahvide sagedused (põhitoon on tavaliselt 49. klahv):

{$ \Large f(klahv) = 440 * 2 ^{klahv-49\over{12}} $}

Järgnev kood mängib helilõigu, kus iga klahv (49-60) kõlab pool sekundit. Klaveri klahvide tekitatud lainevormid on küll keerulisemad kui puhas siinustoon, aga mängime siin lihtsamalt:

from math import *
import struct
from pydub import AudioSegment
from pydub.playback import play

def listToBytes(wave):
    # Teeb helilaine baidijadaks
    waveMax = max(1, max(wave), abs(min(wave)))
    wave = [int((i/waveMax) * (2**15 - 1)) for i in wave]
    return struct.pack(f'<{len(wave)}h', *wave)

seconds = 0.5
samplerate = 44100
A = 1.0 # Heliamplituud (0.0 - 1.0)
t = [i/samplerate for i in range(int(samplerate*seconds))]

notes = range(49, 49+12)
notefrequencies = [440 * 2 ** ((note-49)/12) for note in notes]

sound = AudioSegment.empty()
for freq in notefrequencies:
    # Teeme iga noodi kohta 0.5 sek pikkuse helisignaali
    wave = [A*sin(2*pi*freq * x) for x in t]
    waveBytes = listToBytes(wave)

    notesound = AudioSegment(
        waveBytes,
        frame_rate=samplerate,
        sample_width=2,
        channels=1
    )

    fade = min(len(sound), 5)
    sound = sound.append(notesound, crossfade=fade)

play(sound)

Selgitav video:

Peatüki esimese osa lõpuks toome paar näidet sellest, kuidas lainevorme on võimalik keerulisemaks muuta (kasutatud väärtused: A=1.0, f=444, seconds=5):

wave = [A*sin(2*pi*f * 2*sqrt(x)) for x in t]
wave = [A*sin(2*pi*f * (sqrt(x)*(x-seconds) if x<2 else sqrt(2*x))) for x in t]
wave = [(5**(x+1.5)) % 2 for x in t]

Enesekontrolliküsimused

Heli lindistamine

Lindistamiseks tuleb kasutada teeki sounddevice. Võimalik on seda teha kahel viisil:

import sounddevice as sd

samplerate = 44100
seconds = 2.5

### Lihtsam variant (sd.rec tagastab NumPy massiivi):
data = sd.rec(int(seconds * samplerate), samplerate=samplerate, channels=1, dtype="int16")
print("Lindistamine...")
sd.wait()
print("Valmis.")
sd.play(data, samplerate)
data = data.tobytes()

### Keerukam variant (toimib paralleelselt ülejäänud programmi tööga, kasutab baidimassiivi):
data = bytearray()

def callback(indata, outdata, frames, time, status):
    if status:
        # Kui midagi on valesti
        print(status)
    data.extend(indata)

print("Lindistamine...")
with sd.RawStream(channels=1, samplerate=samplerate, callback=callback, dtype="int16"):
    # Lindistatakse kuni programm viibib siin plokkis
    sd.sleep(int(seconds * 1000))
print("Valmis.")

Sounddevice salvestab heli "toorete" baitidena või numpy massiivina. Et seda mugavalt esitada või faili salvestada, teisendame selle pydub jaoks sobivasse vormingusse:

from pydub import AudioSegment
from pydub.playback import play

# Teisendus:
sound = AudioSegment(
    data, # data peab olema baidijada, NumPy massiivi puhul kasuta data.tobytes()
    frame_rate=samplerate,
    sample_width=2,
    channels=1
)

# Esitamine
play(sound)

# Faili salvestamine
sound.export('fail.wav', format='wav')

Heli töötlemine

Baidijadasid või massiive saab ise "käsitsi" töödelda. See on tegelikult päris mõistlik, kui olemasolevad teegid kõiki mugavusi ei paku. Eriti hea on massiive töödelda teegi numpy abil ning kasutada vajaduse korral näiteks heliefektide teeke, mis numpy massiive, järjendeid või baidijadasid sisendi ja väljundina kasutavad. Ühe pikema nimekirja Pythoni audioteekidest leiab siit lehelt: https://wiki.python.org/moin/PythonInMusic.

Siin peatükis kasutame üht üldisemat teeki nimega pydub, mille dokumentatsiooni leiab siit: https://github.com/jiaaro/pydub/blob/master/API.markdown. Allpool toome välja mõned selle võimalused.

AudioSegment

Teegi pydub keskmes on klass AudioSegment, mis väljendab mingit helilõiku. Üht konkreetset helilõiku kirjeldavat isendit saab luua erinevate funktsioonide abil:

AudioSegment.from_file - Loob helifailist vastava helilõigu objekti.
AudioSegment.silent - Loob etteantud pikkuse ja diskreetimissagedusega vaikusega täidetud helilõigu.
AudioSegment.from_mono_audiosegments - Loob mitmest monoheliga helilõigust ühe mitme helikanaliga helilõigu.
AudioSegment (Klassi vaikekonstruktor) - Loob baidijadast vastava helilõigu.

Ühel helilõigul on järgnevad isendimuutujad:
dBFS - Helilõigu keskmine helitugevus detsibellides võrreldes maksimaalse helitugevusega (dBFS ehk decibels relative to full scale).
channels - Helilõigu helikanalite arv.
sample_width - Baitide arv, mis on ühes kanalis ühe ühiku heli salvestamiseks kasutatud. Bitisügavus = 8*sample_width.
frame_rate - Diskreetimissagedus hertsides.
max - Kõrgeim heliamplituud helilõigus.
max_dBFS - Kõrgeim heliamplituud helilõigus detsibellides võrreldes maksimaalse helitugevusega.
duration_seconds - Helilõigu pikkus sekundites. len(helilõik) annab pikkuse millisekundites.
raw_data - Helilõiku kirjeldav toorete baitide jada.

Helilõikudega saab teha järgmisi tehteid:
helilõik + arv - Annab helilõigu, mille helitugevus on mingi arvu detsibellide võrra suurem.
helilõik - arv - Annab helilõigu, mille helitugevus on mingi arvu detsibellide võrra väiksem.
helilõik * arv - Toimib nagu sõnede korrutamine, annab mingi arv kordi dubleeritud helilõigu.
helilõik + helilõik - Toimib nagu sõnede liitmine, annab ühendatud helilõikudest koosneva pikema helilõigu.
helilõik * helilõik - Helilõigud liidetakse nii, et nad mängivad samaaegselt. Teine helilõik "laotatakse" esimese peale. Kui teine on lühem, siis teda korratakse esimese helilõigu lõpuni. Kui teine on pikem, siis pikem osa lõigatakse ära.

Ühel AudioSegment isendil (ehk siis ühel konkreetsel helilõigul) on järgmised meetodid. NB! Kuna AudioSegment isendid pole muudetavad, siis kõik helilõiku muutvad meetodid tagastavad uue isendi ega muuda algset helilõiku:

MeetodKirjeldus
appendPikendab üht helilõiku teisega. Parameetriga crossfade saab juhtida ülemineku sujuvust ühest lõigust teise.
overlayLaotab helilõigu peale mingi teise helilõigu, nii et nad mängivad samaaegselt. Parameetritega saab määrata, mis hetkest peaks pealelaotatav lõik mängima ja mitu korda korduma (kui ta on lühem).
fadeMuudab sujuvalt helilõigu valjust mingis ajavahemikus.
fade_inAlustab helilõiku seda sujuvalt valjemaks muutes.
fade_outMuudab helilõiku lõpus sujuvalt vaiksemaks.
panSuurendab helitugevust ühes helikanalis ja vähendab samavõrra tugevust teises kanalis.
invert_phaseTagastab helilõigu, mille lained on esimesega vastandfaasis. Saadud helilõiku saab kasutada nt. algse heli nullimiseks.

Vaikuse eemaldamine

Silence on teegi pydub moodul, kust leiab mõned vahendid helilõigust vaikuse tuvastamiseks:

FunktsioonKirjeldus
detect_silenceTagastab järjendi kõikide vahemikega, kus helilõigus tuvastati vaikus.
detect_nonsilentTagastab järjendi kõikide vahemikega, kus helilõigus ei tuvastatud vaikust.
split_on_silenceTagastab helilõikude järjendi, mis koosneb ainult nendest algse heli osadest, kus vaikust ei tuvastatud.

Näide

Allolev video selgitab, kuidas lindistada juttu ja seejärel sellele taustaheli lisada või vaikust eemaldada:

Kasutatud taustaheli: Attach:äikesevihm.wav

from pydub import AudioSegment, silence
from pydub.playback import play
import sounddevice as sd

# Jutu lindistamine
samplerate = 16000
seconds = 7

print("Lindistamine...")
data = sd.rec(int(seconds * samplerate), samplerate=samplerate, channels=1, dtype='int16')
sd.wait()
print("Valmis.")
data = data.tobytes()


# Teeb baitidest helilõigu ja muudab 30db võrra valjemaks
jutt = AudioSegment(
    data,
    frame_rate=samplerate,
    sample_width=2,
    channels=1
) + 30

play(jutt)


# Taustaheliga jutt
# allikas: https://soundbible.com/1907-Thunder.html
taust = AudioSegment.from_file("äikesevihm.wav")

jutu_pikkus = len(jutt)
sujuv_lõpp = taust[jutu_pikkus : jutu_pikkus+1000].fade_out(duration=1000)

taustaga_jutt = jutt.overlay(taust) + sujuv_lõpp
play(taustaga_jutt)


# Ilma pausideta jutt
jutulõigud = silence.split_on_silence(jutt, min_silence_len=100, silence_thresh=-18, keep_silence=75)

pausideta_jutt = AudioSegment.empty()
for lõik in jutulõigud:
    pausideta_jutt = pausideta_jutt + lõik

play(pausideta_jutt)
print("Pausideta jutt tuli", jutu_pikkus-len(pausideta_jutt), "ms lühem!")

Fourier' teisendus

Kui on soovi helisignaalist müra või teatud signaale eemaldada, siis on võimalik uurida levinud tehnikat, mida nimetatakse Fourier' teisenduseks (ingl Fourier transform). Meetod eraldab helisignaalist kõik seal esinevad erineva sagedusega signaalid. Müra (sisuliselt juhuväärtuste jada) koosneb tavaliselt nõrgematest signaalidest, mida on siis võimalik välja filtreerida.

Reaalajas heli esitamine ja peatamine

Mõnikord on vaja mängida ainult osa mingist helist või mängida heli muu programmitöö ajal. Siis peaks funktsiooni play asemel kasutama _play_with_simpleaudio:

from pydub.playback import _play_with_simpleaudio

player = _play_with_simpleaudio(helilõik)
...
if player.is_playing():
    if oota_lõpuni == True:
        player.wait_done()
    else:
        player.stop()

Vahel on aga vaja toota heli hoopis reaalajas sissetuleva sisendi põhjal ning siis tuleks kasutada veidi keerulisemat lahendust. Teegiga sounddevice saab reaalajas heli lindistada või esitada. Esitamine kestab kuni programm viibib allolevas näites toodud with-plokis. Funktsioon RawOutputStream loob väljundvoo, mis püsib avatuna ploki lõpuni (nagu tekstifail!). Sellele tuleb anda parameetrid umbes samamoodi nagu tegime ühe helilõigu loomiseks (dtype="int16" on sisuliselt sama, mis sample_width=2). Parameeter callback peab olema nelja parameetriga funktsioon, mida hakatakse ploki vältel mitu korda sekundis välja kutsuma.

Funktsiooni callback neljast parameetrist on praegu olulised kaks esimest:

  1. Väljundandmete puhver, mis söödetakse kõlarile.
  2. Sämplite arv puhvris

Kui ehitame funktsiooni callback järgneval viisil, saame nt. tuttaval viisil mingi helisignaali konstrueerida ja selle puhvrisse kirjutada. Baitideks teisendamiseks võiks taas kasutada abifunktsiooni listToBytes:

def callback(outdata, frames, time, status):    
    t = [i/frames for i in range(frames)]
    wave = [... for x in t]

    outdata[:] = listToBytes(wave)

with sd.RawOutputStream(channels=1, samplerate=44100, callback=callback, dtype="int16"):
    sd.sleep(1000*seconds)
    ... # Muud tegevused

Enesekontrolliküsimused

Ülesanded

1. Kirjuta programm, mis paneb mõningatest fikseeritud helimustritest või juhuslikest noodikombinatsioonidest iga kord kokku pikema unikaalse meloodia ja mängib seda. Kasuta erinevaid lainevorme!

2. Koosta programm, mis võtab sisendiks mõne inimkõnet sisaldava helifaili (nt. salvestatud loengu) ja teeb selle lühemaks. Selleks võib programm heli kiirendada või sellest tühje lõike välja lõigata.

Vihje: Et heli kaks korda kiiremaks teha, lõika välja iga teine diskreet või kasuta funktsiooni pydub.effects.speedup.

3. Tee graafilise või käsureapõhise kasutajaliidesega programm, kus saab ekraanil või klaviatuuril klahvidele vajutades mingeid noote mängida.

4. Konstrueeri virtuaalne teremin, mida saab mängida hiirt ekraanil ringi liigutades. Peamine tingimus on, et mängitud sagedus peaks sõltuma hiire koordinaatidest reaalajas. Siit võid saada inspiratsiooni: https://theremin.app/.

  • Institute of Computer Science
  • Faculty of Science and Technology
  • University of Tartu
In case of technical problems or questions write to:

Contact the course organizers with the organizational and course content questions.
The proprietary copyrights of educational materials belong to the University of Tartu. The use of educational materials is permitted for the purposes and under the conditions provided for in the copyright law for the free use of a work. When using educational materials, the user is obligated to give credit to the author of the educational materials.
The use of educational materials for other purposes is allowed only with the prior written consent of the University of Tartu.
Terms of use for the Courses environment