{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "669bb5f3",
   "metadata": {},
   "source": [
    "<div align=\"center\">\n",
    "    \n",
    "# ΠΑΝΕΠΙΣΤΗΜΙΟ ΚΡΗΤΗΣ\n",
    "# Τμήμα Επιστήμης Υπολογιστών\n",
    "\n",
    "## HY-215: Εφαρμοσμένα Μαθηματικά για Μηχανικούς\n",
    "## Εαρινό Εξάμηνο 2025-2026\n",
    "### Διδάσκοντες: Γ. Στυλιανού, Γ. Καφεντζής\n",
    "    \n",
    "## Ανάθεση: 2-3-2026\n",
    "## Παράδοση (ηλεκτρονικά, όπως αναφέρεται στην ιστοσελίδα του μαθήματος και στο τέλος του αρχείου αυτού): \n",
    "## Παρασκευή, 16-3-2026, 23:59:59\n",
    "<h1><center>2η Σειρά Εργαστηρίου</center></h1>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "601949cd",
   "metadata": {},
   "source": [
    "Σε κάθε άσκηση `Python`, εισάγουμε πρώτα τις βιβλιοθήκες που θα χρησιμοποιήσουμε: εδώ, θα χρειαστούμε τις `numpy`, `matplotlib`, `Ipython`, `pandas`, και `scipy`, και κάποιες άλλες που θα τις φορτώνουμε στην πορεία.\n",
    "\n",
    "Επιλέξτε το παρακάτω κελί κώδικα και πατήστε Shift + Enter, ώστε να το εκτελέσετε. \n",
    "\n",
    "Το ίδιο θα κάνετε με οποιοδήποτε κελί κώδικα. Αν τροποποιήσετε κάποιο κελί κώδικα, θα πρέπει να το ξανατρέξετε για να δείτε τα αποτελέσματά του."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1e1593ae",
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np                                          # Μαθηματική/αριθμητική βιβλιοθήκη\n",
    "import scipy.io.wavfile as wav                              # Βιβλιοθήκη ανάγνωσης/εγγραφής ήχου\n",
    "from scipy.stats import skew, kurtosis                      # Βιβλιοθήκη στατιστικών\n",
    "import matplotlib.pyplot as plt                             # Βιβλιοθήκη απεικονίσεων\n",
    "from IPython.display import Audio, display, Markdown        # Βιβλιοθήκη παιξίματος αρχείων ήχου\n",
    "import pandas as pd                                         # Βιβλιοθήκη ανάγνωσης και επεξεργασίας αρχείων"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ac9835e1",
   "metadata": {},
   "source": [
    "---\n",
    "---"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5a008d87",
   "metadata": {},
   "source": [
    "### Άσκηση 1 - Πώς μπορούμε να υπολογίσουμε ένα ολοκλήρωμα στον υπολογιστη?"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "00c50afa",
   "metadata": {},
   "source": [
    "Ας κάνουμε μια άσκηση για ζέσταμα πριν μπούμε σε πιο ενδιαφέρουσες ασκήσεις παρακάτω... \n",
    "\n",
    "Η τιμή κάποιων απλών ολοκληρωμάτων μπορεί να υπολογιστεί με αρκετή ακρίβεια με χρήση της `Python`. Ας δούμε πρώτα τι λέει η θεωρία, και συγκεκριμένα ας κάνουμε μια βόλτα από τον Απειροστικό Λογισμό Ι και να θυμηθούμε το"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "19fb50d5",
   "metadata": {},
   "source": [
    "<center> <strong> <font size=5>Ολοκλήρωμα Riemann </strong> </center>\n",
    "<font size=4>\n",
    "<br>\n",
    "Ο Β. Riemann πρότεινε την προσέγγιση ενός ολοκληρώματος από τμηματικά σταθερές συναρτήσεις, των οποίων το αθροιστικό εμβαδό δίνει μια τιμή για το ολοκλήρωμα. Με μαθηματικά:\n",
    "\n",
    "$$ E = \\int_{a}^{b} f(t) dt = \\lim_{\\Delta t \\rightarrow 0} \\sum_{i=1}^{N} f(\\bar{t}_i) \\Delta t$$\n",
    "\n",
    "Τι μας λέει η παραπάνω σχέση; Μας λέει ότι η τιμή ενός ορισμένου ολοκληρώματος μπορεί να βρεθεί αν κάνουμε τα εξής:\n",
    "\n",
    "(i) Χωρίζουμε το διάστημα $[a,b]$ σε $N$ ίσα μέρη μήκους (διάρκειας) $\\Delta t$ το καθένα. Αυτή η *διαμέριση* του διαστήματος λέγεται *ομοιόμορφη*.\n",
    "\n",
    "(ii) Παίρνουμε ένα σημείο $\\bar{t}_i$ σε κάθε ένα από αυτά τα διαστήματα. Για παράδειγμα, το μέσο του διαστήματος $ \\Delta t $ ή όποιο άλλο θέλουμε.\n",
    "\n",
    "(iii) Υπολογίζουμε την τιμή της συνάρτησης $f(t)$ στα σημεία $\\bar{t}_i$ κάθε τμήματος της διαμέρισης.\n",
    "\n",
    "(iv) Έχουμε τώρα $N$ τιμές της $f(t)$, δηλ. τις $f(\\bar{t}_i)$, και πολλαπλασιαζόμενες μαζί με τα $N$ το πλήθος $\\Delta t$, τις προσθέτουμε όλες μαζί για να πάρουμε το αποτέλεσμα.\n",
    "\n",
    "Για να είναι ακριβές το αποτέλεσμα, πρέπει το $\\Delta t$ να είναι όσο μικρότερο γίνεται, δηλ. ιδανικά να ισχύει $\\Delta t \\rightarrow 0$, ώστε να υπάρχει ισότητα μεταξύ ολοκληρώματος και αθροίσματος.\n",
    "\n",
    "**Πρακτικά**, το παραπάνω μεταφράζεται στην προσέγγιση της επιφάνειας κάτω από τη συνάρτηση $f(t)$ από ορθογώνια παραλληλόγραμμα με \"ύψος\" $f(\\bar{t}_i)$ και \"πλάτος\" $\\Delta t$. Γνωρίζουμε από τη Γεωμετρία ότι κάθε παραλληλόγραμμο έχει επιφάνεια \n",
    "\n",
    "$$E_i = f(\\bar{t}_i)\\Delta t$$\n",
    "\n",
    "και η προσέγγιση του ολοκληρώματος, υποθέτοντας ότι έχουμε $N$ τέτοια παραλληλόγραμμα, θα είναι\n",
    "\n",
    "$$E \\approx \\sum_{i=1}^N E_i =\\sum_{i=1}^N f(\\bar{t}_i)\\Delta t $$\n",
    "\n",
    "Όταν το \"πλάτος\" $\\Delta t$ κάθε παραλληλογράμμου γίνεται όλο και μικρότερο, τόσο καλύτερη η προσέγγιση της επιφάνειας (δηλ. του ολοκληρώματος που ζητάμε) από το άθροισμα των επιφανειών των παραλληλογράμμων."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "25ac32f8",
   "metadata": {},
   "source": [
    "Η διαδικασία φαίνεται στο παρακάτω σχήμα.\n",
    "\n",
    "![riemann](./riemann.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7f90c4f4",
   "metadata": {},
   "source": [
    "I. Ας δούμε πως θα υλοιποιήσουμε αριθμητικά στη `Python` το ολοκλήρωμα Riemann\n",
    "\n",
    "$$\\int_{-\\infty}^{+\\infty}\\left(-\\frac{1}{3}\\right)e^{-2(t-2)}u(t-2)dt = \\int_2^{+\\infty}\\left(-\\frac{1}{3}\\right)e^{-2(t-2)}dt = -\\frac{1}{6}$$\n",
    "\n",
    "Έχουμε:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7999d317",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Ορίζουμε τον άξονα του χρόνου από t=\"2 + κάτι μικρό\" (λόγω της βηματικής που ξεκινά από το t=2)\n",
    "# ως μια μεγάλη τιμή που προσομοιώνει το άπειρο, π.χ. την τιμή 1000, τμηματοποιώντας \n",
    "# τον ανά Dt = 0.001\n",
    "Dt = 0.001\n",
    "\n",
    "# άξονας του χρόνου \n",
    "t = np.arange(start=2+Dt, stop=1000, step=Dt)\n",
    "\n",
    "# Η συνάρτηση x(t) ορίζεται ως\n",
    "x = (-1/3) * np.exp(-2*(t-2))\n",
    "\n",
    "# Υπολογίζουμε με τον παρακάτω τρόπο το ολοκλήρωμα Riemann:\n",
    "apotelesma = Dt * sum(x)\n",
    "\n",
    "# και το τυπώνουμε:\n",
    "print(f'{apotelesma:.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0596357e",
   "metadata": {},
   "source": [
    "Ας το επιβεβαιώσουμε:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4331380e",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Επιβεβαίωση:\n",
    "print(f'{-1/6:.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "32f82fc8",
   "metadata": {},
   "source": [
    "II. Ας υπολογίσουμε τώρα το (διάσημο) ολοκλήρωμα\n",
    "\n",
    "$$ \\int_{0}^{\\pi /2} \\left(\\frac{t}{\\sin(t)}\\right)^2 dt = \\pi \\ln (2) $$\n",
    "\n",
    "Αρχικά πρέπει να προσέξουμε ότι η συνάρτηση μέσα στο ολοκλήρωμα δεν ορίζεται για $t=0$. Οπότε:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "055f4d00",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Ορίζουμε τον άξονα του χρόνου από \"0 + κάτι_μικρό\" ως π/2 για να εξασφαλίσουμε ότι\n",
    "# δε μηδενίζεται ο παρονομαστής της συνάρτησής μας. Η τμηματοποίηση του άξονα είναι\n",
    "# ξανά ανά Dt που τώρα το θέτουμε ίσο με 0.00001.\n",
    "Dt = 1e-5 # Math notation: 1e-a = 10**(-a)\n",
    "\n",
    "t = np.arange(start=Dt, stop=np.pi/2, step=Dt)\n",
    "\n",
    "# Ορίζουμε τη συνάρτηση t/sin(t)\n",
    "f = (t/np.sin(t))\n",
    "\n",
    "# Υψώνουμε στο τετράγωνο\n",
    "f_sq = f**2\n",
    "\n",
    "# Υπολογίζουμε το ολοκλήρωμα Riemann όπως και πριν:\n",
    "apotelesma = Dt * sum(f_sq)\n",
    "print(f'{apotelesma:.4f}')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d81217c6",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Επιβεβαίωση:\n",
    "print(f'{np.pi*np.log(2):.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b6b07605",
   "metadata": {},
   "source": [
    "III. Με βάση τα παραπάνω, επιβεβαιώστε αριθμητικά στην `Python` τα ολοκληρώματα:\n",
    "\n",
    "a. $$ \\int_{0}^{1} \\frac{\\ln(t)}{1+t}dt = -\\frac{\\pi^2}{12} $$\n",
    "\n",
    "\n",
    "b. $$ \\int_{1}^{2} \\frac{(t+2)(t-2)}{t^2} dt = -1 $$\n",
    "\n",
    "\n",
    "c. $$ \\int_{0}^{+\\infty} \\frac{t}{e^t - 1} dt = \\frac{\\pi^2}{6} $$"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2f8c65cc",
   "metadata": {},
   "outputs": [],
   "source": [
    "# a.\n",
    "Dt = 1e-5 \n",
    "t1 = # INSERT CODE\n",
    "f1 = # INSERT CODE\n",
    "apotelesma1 = # INSERT CODE\n",
    "print(f\"a. (Riemmann)   = {apotelesma1:.4f}\")\n",
    "print(f\"a. (Επιβεβαίωση): -pi**2/12 = {-np.pi**2/12:.4f}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "36685b99",
   "metadata": {},
   "outputs": [],
   "source": [
    "# b.\n",
    "Dt = 1e-5\n",
    "t2 = # INSERT CODE\n",
    "f2 = # INSERT CODE\n",
    "apotelesma2 = # INSERT CODE\n",
    "print(f\"b. (Riemmann) = {apotelesma2:.4f}\")\n",
    "print(f\"b. (Επιβεβαίωση): {-1:.4f}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ba47a3c7",
   "metadata": {},
   "outputs": [],
   "source": [
    "# c.\n",
    "Dt = 1e-5\n",
    "t3 = # INSERT CODE\n",
    "f3 = # INSERT CODE\n",
    "apotelesma3 = # INSERT CODE\n",
    "print(f\"c. (Riemmann)   = {apotelesma3:.4f}\")\n",
    "print(f\"c. (Επιβεβαίωση): pi^2/6 = {np.pi**2/6:.4f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "55fb2b49",
   "metadata": {},
   "source": [
    "---\n",
    "---"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9daaf158",
   "metadata": {},
   "source": [
    "### 2η Άσκηση - Πώς καταλαβαίνει το κινητό μας τηλέφωνο πότε μιλάμε και πότε όχι?"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0277e747",
   "metadata": {},
   "source": [
    "Μόνο και μόνο με τις έννοιες της *ενέργειας* και των απλών πρότυπων σημάτων, όπως ο *τετραγωνικός παλμός*, που έχετε μάθει, μπορείτε να δείτε μερικές εφαρμογές όπως η \n",
    "<center><font size=6>\n",
    "Aνίχνευση δραστηριότητας ομιλίας - voice activity detection. \n",
    "</center>\n",
    "<font size=4>\n",
    "Αλγόριθμοι που υλοποιούν τέτοιες εφαρμογές υπάρχουν σε κάθε κινητό τηλέφωνο, όπως επίσης και σε συστήματα αναγνώρισης ομιλίας όπως η Siri, το Google Assistant, και η Amazon Alexa, αλλά και σε συστήματα επικοινωνιών όπως το Zoom, το Skype, και το Microsoft Teams.\n",
    "\n",
    "Μια *ιδιαίτερα* απλοϊκή υλοποίηση ενός τέτοιου αλγορίθμου είναι η εξής :\n",
    "- Διαβάζετε ένα αρχείο ομιλίας στην Python που σας δίνεται.\n",
    "- Παραθυροποιείτε με χρήση τετραγωνικού παλμού το σήμα σας, \"κόβοντας\" έτσι ένα τμήμα του.\n",
    "    - Θεωρητικά, αυτό αντιστοιχεί σε πολλαπλασιασμό του σήματος με ένα τετραγωνικό παλμό ($\\mathrm{rect}(\\cdot)$)\n",
    "    - Η παραθυροποίηση γίνεται πρακτικά έμμεσα, απομονώνοντας ένα κομμάτι του σήματος μέσω array slicing. \n",
    "        - Άρα πρακτικά δεν πολλαπλασιάζουμε κάποιο σήμα με τον τετραγωνικό παλμό, αλλά **θεωρούμε** ότι το κάνουμε για να καταλάβουμε περισσότερα όσον αφορά το χώρο της συχνότητας, που θα ακολουθήσει αργότερα στο μάθημα.\n",
    "        - Αν όμως το παράθυρό μας είναι ένας άλλος παλμός, π.χ. τριγωνικός, τότε **ναι**, γίνεται **ρητά** ο πολλαπλασιασμός του τμήματος του σήματος με τον τριγωνικό παλμό. \n",
    "- Για κάθε παραθυροποιημένο σήμα, υπολογίζετε την ενέργεια του από τη γνωστή σχέση της θεωρίας.\n",
    "- Ανάλογα με την τιμή της ενέργειας, αποφασίζετε αν το παραθυροποιημένο σήμα περιέχει ομιλία ή όχι.\n",
    "- Προχωράτε στο επόμενο παραθυροποιημένο σήμα και επαναλαμβάνετε, ως το τέλος του σήματος."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "913b10f8",
   "metadata": {},
   "source": [
    "Ερωτήματα που προκύπτουν από την ανάγνωση του παραπάνω αλγορίθμου:\n",
    "\n",
    "(α') Πόση είναι η διάρκεια του παραθυροποιημένου σήματος; Μια τιμή κοντά στα $0.03$ s $=30$ ms είναι καλή. Δεν υπάρχει βέλτιστη διάρκεια, υπάρχουν trade-offs είτε έχει μεγάλη είτε μικρή διάρκεια.\n",
    "\n",
    "(β') Πόση απόσταση ϑα έχουν τα διαδοχικά παραθυροποιημένα σήματα μεταξύ τους; Στην πράξη, θα πρέπει όχι απλά να είναι διαδοχικά, αλλά να έχουν και κάποιο *ποσοστό επικάλυψης*. Για τους δικούς μας σκοπούς, θα χρησιμοποιήσουμε $0\\%$ επικάλυψη, δηλ. τα παραθυροποιημένα σήματα θα είναι ακριβώς διαδοχικά, χωρίς \"κενά\" μεταξύ τους (αφού κάτι τέτοιο θα σήμαινε απώλεια εκτίμησης δραστηριότητας ομιλίας).\n",
    "\n",
    "(γ') Πώς αποφασίζω με ϐάση την ενέργεια αν το εκάστοτε παραθυροποιημένο σήμα είναι τμήμα ομιλίας; Αυτή είναι μια ερώτηση που δέχεται αρκετές απαντήσεις - προσεγγίσεις. Ξανά μια απλή προσέγγιση είναι ότι στην αρχή του σήματος, υπάρχουν μερικά δευτερόλεπτα σιωπής - μπορείτε να εκτιμήσετε την ενέργεια σε αυτά τα παραθυροποιημένα σήματα και να αποφασίζετε ϐάσει αυτής (με κάποιο στατιστικό κριτήριο, όπως για παράδειγμα \"*κάθε παραθυροποιημένο σήμα να έχει ενέργεια μεγαλύτερη του διπλάσιου της ενέργειας των παραθυροποιημένων σημάτων σιωπής*\") για τα επόμενα τμήματα ομιλίας."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2defde3c",
   "metadata": {},
   "source": [
    "Το παρακάτω σχήμα δείχνει την τμηματοποίηση και τον υπολογισμό ενέργειας για κάθε παράθυρο (frame) σήματος.\n",
    "\n",
    "![rectwin2](./rectwindows.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f0b59d02",
   "metadata": {},
   "source": [
    "Αν και υπάρχει ξεχωριστή εξίσωση για τον υπολογισμό της ενέργειας ενός σήματος **διακριτού χρόνου** (και θα τη δούμε στην επόμενη άσκηση), όπως ένα .WAV αρχείο, θα χρησιμοποιήσουμε το ολοκλήρωμα Riemann για να υπολογίσουμε την ενέργεια, όπως είδατε στην προηγούμενη άσκηση (και θα δείτε και στο ακόλουθο toy example).\n",
    "\n",
    "Το σήμα ομιλίας που έχουμε ηχογραφήσει και σας δίνουμε είναι *ψηφιακό* και όταν το διαβάζετε στην `Python`, το μετατρέπετε σε *διακριτού χρόνου*, αποθηκεύοντας τα δείγματά του σε ένα διάνυσμα. \n",
    "\n",
    "Μέχρι τότε όμως, ας καταλάβουμε λίγο τι συμβαίνει με ένα πιο ελεγχόμενο toy-παράδειγμα. Έστω ότι το σήμα ομιλίας διακριτού χρόνου που διαβάζετε είναι η ακολουθία από αριθμούς αποθηκευμένους στο διάνυσμα ```x``` όπως παρακάτω. Ας το δούμε."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3c51db25",
   "metadata": {},
   "outputs": [],
   "source": [
    "x = np.asarray([1, 0.5, 1.2, -1.7, 2.4, -0.3, -1.1, 0.6, -0.9])  # τυχαίο σήμα - υποτίθεται σήμα ομιλίας\n",
    "\n",
    "print(f\"Το σήμα ομιλίας διακριτού χρόνου είναι το x = {x}\")      # βλέπουμε τις τιμές του\n",
    "plt.stem(np.arange(0,9), x)                                      # τυπώνουμε τις τιμές του\n",
    "plt.xlabel('Διακριτός χρόνος')                                   # ομορφαίνουμε\n",
    "plt.ylabel('Πλάτος σήματος')                                     # ομορφαίνουμε"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0b32e6fe",
   "metadata": {},
   "source": [
    "Οι τιμές που βλέπετε αποτελούν **δείγματα** του σήματος ομιλίας συνεχούς χρόνου που καταγράφηκε. Τα δείγματα αυτά λήφθηκαν ανά $1/F_s$ δευτερόλεπτα πραγματικού χρόνου, με $F_s$ να είναι η συχνότητα δειγματοληψίας, που σας δίνεται έτοιμη κάθε φορά που ανοίγετε ένα αρχείο ήχου στην `Python`. Αν, για παράδειγμα, η συχνότητα δειγματοληψίας είναι $16000$ Hz, τότε το παραπάνω σήμα $9$ δειγμάτων προέρχεται από ένα σήμα συνεχούς χρόνου διάρκειας \n",
    "\n",
    "$$(9-1)/16000 = 8/16000 = 500 \\text{ μs} = 0.5 \\text{ ms} = 0.0005 \\text{ s}$$\n",
    "\n",
    "Πραγματικά πολύ μικρής διάρκειας, αλλά είπαμε: toy-παράδειγμα. 😉 \n",
    "\n",
    "Για να φτάσουμε στα $30$ ms που αναφέρει παραπάνω η άσκηση, χρειαζόμαστε $481$ δείγματα - αφού $(481-1)/16000 = 0.03$ s $= 30$ ms. Ας συνεχίσουμε όμως σε αυτό το toy example και ας υποθέσουμε όμως ότι θέλουμε να υπολογίσουμε την ενέργεια ανά $187.5$ μs, δηλ. ανά μόλις $N=3$ δείγματα. Τότε θα χωρίσουμε το σήμα σε $N-$άδες (τριάδες, εν προκειμένω), που ουσιαστικά αυτό αντιστοιχεί σε διαδοχικούς πολλαπλασιασμούς με μη επικαλυπτόμενους τετραγωνικούς παλμούς μοναδιαίου πλάτους και διάρκειας $187.5$ μs, ήτοι διάρκειας $N$ (τριών) δειγμάτων. \n",
    "\n",
    "Έτσι προκύπτουν (δείτε ξανά το διάνυσμα ```x``` παραπάνω) τρεις τριάδες, $(1, 0.5, 1.2), (−1.7, 2.4,−0.3), (−1.1, 0.6,−0.9)$, που καθεμιά είναι ένα παράθυρο σήματος, και θα υπολογίσουμε την ενέργειά καθεμιάς. Θα χρησιμοποιήσουμε μία λίστα (https://www.w3schools.com/python/python_lists.asp) της `Python`, στην οποία μπορούμε να *προσαρτήσουμε* (append) οποιαδήποτε μεταβλητή οποιουδήποτε μεγέθους, για να κρατάμε την ενέργεια καθενός παραθύρου."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "19775344",
   "metadata": {},
   "outputs": [],
   "source": [
    "Fs = 16000   # Έστω μια συχνότητα δειγματοληψίας\n",
    "Dt = 1/Fs    # Περίοδος δειγματοληψίας (κάθε πόσα δευτερόλεπτα πήραμε ένα δείγμα από την ηχογράφησή μας)\n",
    "N = 3        # Διάρκεια παραθύρου σε δείγματα : αυτό αντιστοιχεί σε 187.5 μικρο-δευτερόλεπτα πραγματικού χρόνου\n",
    "\n",
    "# Τμηματοποίηση σε διαδοχικές N-άδες και υπολογισμός ενέργειας τους\n",
    "x1 = x[0:N]     # από 0 ως Ν-1\n",
    "x2 = x[N:2*N]   # από Ν ως 2Ν-1\n",
    "x3 = x[2*N:3*N] # από 2Ν ως 3Ν-1\n",
    "\n",
    "E = [] # μια άδεια λίστα για να μαζεύουμε τις ενέργειες των Ν-άδων\n",
    "E.append(Dt * np.sum(x1**2)) # Riemann άθροισμα για το 1ο τμήμα\n",
    "E.append(Dt * np.sum(x2**2)) # Riemann άθροισμα για το 2ο τμήμα\n",
    "E.append(Dt * np.sum(x3**2)) # Riemann άθροισμα για το 3ο τμήμα\n",
    "\n",
    "print(f\"E1 = {E[0]}\\nE2 = {E[1]}\\nE3 = {E[2]}\") \n",
    "# θα πρέπει να λάβετε: \n",
    "# E1 = 0.000168125, \n",
    "# E2 = 0.000546249, \n",
    "# E3 = 0.00014875"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "276ca0cc",
   "metadata": {},
   "source": [
    "Βλέπετε ότι κάθε τμήμα από το αρχικό σήμα έχει διαφορετική τιμή για την ενέργειά του. \n",
    "\n",
    "Ας επιστρέψουμε τώρα στο πλήρες, πραγματικό παράδειγμα, όπου θα υλοποιήσουμε πιο αυτοματοποιημένα τα παραπάνω, καθώς και την επιλογή της δραστηριότητας ομιλίας με βάση την ενέργεια κάθε τμήματος σήματος.\n",
    "\n",
    "Ας γράψουμε κώδικα που διαβάζει ένα σήμα φωνής και μας υπολογίζει ένα διάνυσμα της ενέργειάς του σε κάποιες χρονικές στιγμές που αντιστοιχούν στα κέντρα των παραθύρων που αναλύουμε.\n",
    "\n",
    "Η ιδέα είναι η εξής:\n",
    "\n",
    "- Το σήμα ομιλίας θα αποθηκευτεί σε ένα ``numpy`` array.\n",
    "- Θα το προσπελαύνουμε ανά μη επικαλυπτόμενα τμήματα, χρησιμοποιώντας array slicing, όπως στο παραπάνω toy example.\n",
    "- Για κάθε τμήμα, θα υπολογίζουμε την ενέργειά του.\n",
    "- Θα την αποθηκεύουμε σε ένα άλλο array, με τέτοιο τρόπο ώστε η $i-$οστή τιμή του array να αντιστοιχεί στο $i-$οστό τμήμα.\n",
    "- Μόλις μαζέψουμε όλες τις ενέργειας, θα αποφασίσουμε ένα κατώφλι (threshold) με το οποίο θα συγκρίνουμε αυτές τις τιμές για να ταξινομούμε κάθε frame που αντιστοιχεί στις υπολογισμένες ενέργειες στην κατηγορία \"speech\" ή \"non-speech\".\n",
    "\n",
    "Συμπληρώστε τον παρακάτω σκελετό."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6f201635",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Φόρτωση αρχείου ήχου στην Python (Fs είναι η συχνότητα δειγματοληψίας)\n",
    "Fs, s = wav.read('speech.wav')\n",
    "\n",
    "# Βήμα στο χρόνο (κάθε πότε πήραμε δείγμα από την πραγματική ηχογράφηση? κάθε 1/Fs δευτερόλεπτα)\n",
    "Dt = 1/Fs\n",
    "\n",
    "# Κανονικοποίηση του σήματος σε πλάτη [-1,1] (τεχνική λεπτομέρεια, μην τη λάβετε υπ'όψη)\n",
    "s = s/np.max(np.abs(s))\n",
    "\n",
    "# θέλουμε παράθυρα 30 ms στο χρόνο, σε πόσα δείγματα αντιστοιχούν? Αν 1 δευτερόλεπτο αντιστοιχεί σε Fs δείγματα, πόσα δείγματα αντιστοιχούν σε 0.03 δευτερόλεπτα? Χρησιμοποιήστε τη round().\n",
    "T = # INSERT CODE\n",
    "\n",
    "# Συνολική διάρκεια σήματος\n",
    "L = len(s)\n",
    "\n",
    "# Πόσα παράθυρα διάρκειας T δειγμάτων που βρηκαμε παραπάνω, μη επικαλυπτόμενα, χωρούν σε όλο το σήμα διάρκειας L δειγμάτων? Χρησιμοποιήστε τη round().\n",
    "N = # INSERT CODE\n",
    "\n",
    "# Δέσμευση μνήμης - αντίστοιχο της calloc στη C\n",
    "Energy = np.zeros(N)\n",
    "Time = np.zeros(N)\n",
    "\n",
    "for i in range(1, N):\n",
    "    \n",
    "    # Χρησιμοποιώντας το δείκτη i, τη διάρκεια T, και τον τελεστή int(), κόψτε κάθε φορά ένα τμήμα του σήματος s διάρκειας T\n",
    "    windowed_speech = # INSERT CODE\n",
    "\n",
    "    # Υπολογίστε την ενέργεια του τμήματος σήματος windowed_speech (όπως δείξαμε παραπάνω)\n",
    "    Energy[i] = # INSERT CODE\n",
    "    \n",
    "    # Κέντρο του κάθε τμήματος (το δείγμα στη μέση του τμήματος, π.χ. ένα τμήμα διάρκειας 200 δειγμάτων έχει κέντρο το 100)\n",
    "    Time[i] = round(i*T+T/2)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8af13e26",
   "metadata": {},
   "source": [
    "Ας επιλέξουμε κατώφλι απόφασης με βάση τα frames της σιωπής στην αρχή του αρχείου. Όπως είπαμε, τα πρώτα 2 δευτερόλεπτα της ηχογράφησης (δηλ. τα πρώτα $2\\cdot F_s$ δείγματα) έχουν σιωπή. Συμπληρώστε κατάλληλα παρακάτω."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "eba1ead2",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Κατώφλι απόφασης\n",
    "No_Silence_Frames = round(2*Fs/T)             # το πληθος των frames που αντιστοιχούν στα πρώτα 2 δευτερόλεπτα\n",
    "Threshold = Energy[:No_Silence_Frames].mean() # έστω ότι το κατώφλι είναι η μέση ενέργεια αυτών των πρώτων frames που αντιστοιχούν στα πρώτα 2 seconds\n",
    "\n",
    "# Δέσμευση μνήμης για το διάνυσμα απόφασης (διάνυσμα με μηδενικά)\n",
    "D = np.zeros(N)\n",
    "\n",
    "# Διατρέχετε το διάνυσμα της ενέργειας που υπολογίσαμε ανά frame στον προηγούμενο κώδικα και αποφασίζετε \n",
    "# - 1 αν η ενέργεια του i-οστού frame είναι > από το κατώφλι, \n",
    "# - 0 αν η αντίστοιχη ενέργεια για το i-οστό frame είναι < κατώφλι\n",
    "for i in range(N):\n",
    "    if # INSERT CODE:\n",
    "         D[i] = # INSERT CODE"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "04968535",
   "metadata": {},
   "source": [
    "Τώρα μπορούμε να \"ενώσουμε\" τις εκτιμήσεις μας και να απεικονίσουμε το αποτέλεσμα."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "699ffc54",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Γράφημα\n",
    "plt.figure(figsize=(10,5))\n",
    "\n",
    "t = np.arange(start=0, stop=L*Dt, step=Dt)               # άξονας χρόνου με βήμα Dt\n",
    "n = np.arange(start=0, stop=L, step=1)                   # άξονας διακριτού χρόνου για κάθε δείγμα\n",
    "Din = np.interp(x=n, xp=Time, fp=D)                      # γραμμική παρεμβολή\n",
    "\n",
    "plt.plot(t, Din, color='tab:blue', linewidth=2)            # γράφημα απόφασης\n",
    "plt.plot(t, s/max(abs(s)), color='tab:red', linewidth=0.4) # γράφημα σήματος\n",
    "\n",
    "plt.title('Voice Activity Detector')                     # Ομορφαίνουμε\n",
    "plt.xlabel('Time (s)')                                   # --- \" -----\n",
    "plt.grid(linestyle=':')                                  # --- \" -----\n",
    "\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2a3aa7d7",
   "metadata": {},
   "source": [
    "Αν τα κάνετε όλα σωστά, θα πάρετε κάτι **πολύ κοντά** στο παρακάτω σχήμα:\n",
    "\n",
    "![vad2](./output.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dc8145bc",
   "metadata": {},
   "source": [
    "α) Επαναλάβετε την παραπάνω άσκηση με χρήση της δικής σας φωνής! Ηχογραφήστε τον εαυτό σας, για 5-7 δευτερόλεπτα, λέγοντας μια τυχαία πρόταση μεγάλης διάρκειας αλλά <span style=\"font-size:1.5em;\">κρατώντας τα δυο πρώτα δευτερόλεπτα σιωπηλά</span>. \n",
    "\n",
    "Χρησιμοποιήστε συχνότητα δειγματοληψίας $16000$ Hz, και μονοκαναλική ηχογράφηση. Προτεινόμενο πρόγραμμα ηχογράφησης:\n",
    "<center><font size=6> Audacity (https://www.audacityteam.org/)<font size=4></center>\n",
    "Αν θέλετε, συμπεριλάβετε στην παράδοσή σας το αρχείο αυτό. Θα διαγραφεί μετά τη διόρθωση της άσκησης.\n",
    "\n",
    "Δείτε το βιντεοπαράδειγμα ηχογράφησης με το Audacity (το βίντεο δεν έχει ήχο) που συνοδεύει τα αρχεία του εργαστηρίου. Το παραγόμενο αρχείο πρέπει να μπει στον ίδιο κατάλογο με το παρόν .ipynb αρχείο."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5fdd1953",
   "metadata": {},
   "source": [
    "<span style=\"font-size:1em;\">Αν δεν το καταφέρετε, χρησιμοποιήστε το αρχείο `au-bout-du-telephone.wav` που σας δίνεται.</span>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0b608b4e",
   "metadata": {},
   "outputs": [],
   "source": [
    "# INSERT CODE HERE\n",
    "\n",
    "# Αντιγράψτε τον προηγούμενο κώδικα και απλά προσαρμόστε τον στο δικό σας σήμα\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "# Μην ξεχάσετε να τυπώσετε στην οθόνη το γράφημα που προκύπτει!\n",
    "\n",
    "# Κώδικας για γράφημα\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "# END OF CODE"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bb4a6c06",
   "metadata": {},
   "source": [
    "β) Κάντε το ίδιο για τα δυο αρχεία που σας δίνονται, ``male_speech.wav`` και ``female_speech.wav``."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "457b443b",
   "metadata": {},
   "outputs": [],
   "source": [
    "# INSERT CODE HERE\n",
    "\n",
    "# Αντιγράψτε τον προηγούμενο κώδικα και απλά προσαρμόστε τον στο male_speech.wav\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "# Μην ξεχάσετε να τυπώσετε στην οθόνη το γράφημα που προκύπτει!\n",
    "\n",
    "# Κώδικας για γράφημα\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "# END OF CODE"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2adc176f",
   "metadata": {},
   "outputs": [],
   "source": [
    "# INSERT CODE HERE\n",
    "\n",
    "# Αντιγράψτε τον προηγούμενο κώδικα και απλά προσαρμόστε τον στο female_speech.wav\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "# Μην ξεχάσετε να τυπώσετε στην οθόνη το γράφημα που προκύπτει!\n",
    "\n",
    "# Κώδικας για γράφημα\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "# END OF CODE"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d4accb2b",
   "metadata": {},
   "source": [
    "---\n",
    "---"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f3f0fd92",
   "metadata": {},
   "source": [
    "### 3η Άσκηση - Μπορεί ο υπολογιστής να καταλάβει πως αισθανόμαστε \"ακούγοντας\" την ομιλία μας?"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1fc569c1",
   "metadata": {},
   "source": [
    "Ένα πολύ σημαντικό και ερευνητικά ανοιχτό πρόβλημα στον τομέα της Επικοινωνίας Ανθρώπου-Υπολογιστή είναι η **αναγνώριση του συναισθήματος ενός ομιλητή**. Το πρόβλημα αυτό είναι ιδιαίτερα πολύπλοκο και πιθανότατα λύνεται με πολυτροπικές μεθόδους (multi-modal approaches), δηλ. μεθόδους που λαμβάνουν υπ'όψη τα λόγια, την εικόνα (φωτογραφία ή βίντεο του ομιλητή), και την **προσωδία** του ομιλητή. \n",
    "\n",
    "Στην άσκηση αυτή θα παίξουμε με μια πολύ απλή μέθοδο αναγνώρισης συναισθήματος από την ομιλία, χωρίς να λαμβάνουμε υπ'όψη τα λόγια αλλά μόνο την προσωδία, δηλ. το **πως** μιλάει κάποιος (την τονικότητα, το ρυθμό, το χρώμα, κλπ). \n",
    "\n",
    "Φυσικά, ακόμα **δεν είστε σε θέση να καταλάβετε πολλά από αυτά αλλά ελπίζουμε να σας κεντρίσουμε το ενδιαφέρον**. 😊 \n",
    "\n",
    "*Ο κώδικας που θα χρειαστεί να γράψετε είναι ελάχιστος σε αυτήν την άσκηση. Καλείστε όμως να κατανοήσετε τη μεθοδολογία και (μερικώς, σε αδρές γραμμές) τον έτοιμο κώδικα.*"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "493858ab",
   "metadata": {},
   "source": [
    "- **Βάση δεδομένων**: \n",
    "\n",
    "Σας δίνεται η βάση δεδομένων ```Berlin-EMOdB```, μια αρκετά παλιά και γνωστή βάση δεδομένων συναισθηματικής ομιλίας στα Γερμανικά με 535 συνολικά αρχεία WAV. Η ομιλία είναι *acted*, δηλ. δεν είναι φυσικού συναισθήματος αλλά υποκριτικού (από ηθοποιούς). Πάμε να σετάρουμε τα βασικά paths."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "35be0efe",
   "metadata": {},
   "outputs": [],
   "source": [
    "import re\n",
    "from pathlib import Path\n",
    "\n",
    "RNG_SEED = 1337\n",
    "np.random.seed(RNG_SEED)\n",
    "\n",
    "# -------------> Αλλάξτε το παρακάτω path ώστε να δείχνει στον κατάλογο wav της βάσης στο δίσκο σας: <----------------- \n",
    "DATA_DIR = Path(\"C:/Users/kafge/OneDrive - Πανεπιστήμιο Κρήτης/ΗΥ215/Python/2026/HY215-Lab2-2026/EmoDB/wav\")\n",
    "\n",
    "assert DATA_DIR.exists(), f\"Δε βρίσκω τον κατάλογο: {DATA_DIR.resolve()}\"\n",
    "\n",
    "# Διαβάζουμε τα WAV files και τα κρατάμε σε μια λίστα με όνομα wav_files\n",
    "wav_files = sorted(DATA_DIR.glob(\"*.wav\"))\n",
    "\n",
    "print(f\"Βρήκαμε {len(wav_files)} WAV αρχεία στον κατάλογο {DATA_DIR.resolve()}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "134f7598",
   "metadata": {},
   "source": [
    "Αν όλα πήγαν καλά στο παραπάνω κελί και βρήκατε τα $535$ αρχεία που πρέπει, ας δούμε τη βάση δεδομένων αυτή **περιγραφικά**: πόσα αρχεία WAV έχει, πώς είναι οργανωμένη, πόσους ομιλητές έχει, πόσα συναισθήματα, κλπ.\n",
    "\n",
    "Εδώ πρέπει να γνωρίζετε ότι η βάση αυτή περιγράφει τα αρχεία της με το εξής στυλ (ενδεικτικό αρχείο): `03a01Fa.wav`  \n",
    "Αυτή η ονοματοδοσία κωδικοποιεί τα πάντα για το αρχείο αυτό. Για παράδειγμα, το πρώτο '03' σημαίνει ότι μιλάει ο ομιλητής 03 και το γράμμα F δηλώνει το συναίσημα της \"Χαράς\".\n",
    "\n",
    "Κάθε συναίσθημα κωδικοποιείται στο προτελευταίο γράμμα του WAV αρχείου ως εξής:\n",
    "\n",
    "Σύμβολο : συναίσθημα (γερμανικό - ελληνικό όνομα)\n",
    "- `W` : anger (Wut - Θυμός)\n",
    "- `L` : boredom (Langeweile - Βαρεμάρα)\n",
    "- `E` : disgust (Ekel - Αηδία)\n",
    "- `A` : fear/anxiety (Angst - Φόβος)\n",
    "- `F` : happiness (Freude - Χαρά)\n",
    "- `T` : sadness (Trauer - Θλίψη)\n",
    "- `N` : neutral (Ουδέτερο)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1a002ed6",
   "metadata": {},
   "source": [
    "Ας ορίσουμε λοιπόν αυτήν την αντιστοίχιση και ας γράψουμε μια **κανονική έκφραση** (που ίσως ήδη ξέρετε ή αν όχι, θα μάθετε σε άλλα μαθήματα) για να εξάγουμε εύκολα αυτά τα στοιχεία από τη συμβολοσειρά του κάθε αρχείου WAV."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8f7fd3ee",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Μοτίβο για κάθε όνομα αρχείου (regular expression (RE) - κανονική έκφραση):\n",
    "PRIMARY_RE = re.compile(r\"^(?P<spk>\\d{2})(?P<txt>[a-z])(?P<utt>\\d{2})(?P<emo>[A-Z])(?P<int>[a-z])\\.wav$\", re.IGNORECASE)\n",
    "\n",
    "# Αντιστοίχιση\n",
    "EMO_MAP = {\n",
    "    \"W\": \"Θυμός\",\n",
    "    \"L\": \"Βαρεμάρα\",\n",
    "    \"E\": \"Αηδία\",\n",
    "    \"A\": \"Φόβος\",\n",
    "    \"F\": \"Χαρά\",\n",
    "    \"T\": \"Θλίψη\",\n",
    "    \"N\": \"Ουδέτερο\",\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7c524713",
   "metadata": {},
   "source": [
    "Πάμε να διαβάσουμε τα αρχεία της βάσης. \n",
    "\n",
    "Ξεκινάμε δημιουργώντας μια συνάρτηση που εντοπίζει τον ομιλητή και το συναίσθημα με βάση το μοτίβο της ονοματοδοσίας του αρχείου που ορίσαμε παραπάνω."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "879ca385",
   "metadata": {},
   "outputs": [],
   "source": [
    "def parse_emodb_filename(pathname):\n",
    "    \"\"\"\n",
    "    Επιστρέφει (speaker_id, emotion_code)\n",
    "    \"\"\"\n",
    "    m = PRIMARY_RE.match(pathname.name)  # έλεγξε το όνομα αρχείου με την regular expression ώστε να \"ματσάρεις\" τα στοιχεία\n",
    "    spk = m.group(\"spk\")                 # πάρε τον κωδικό ομιλητή\n",
    "    emo = m.group(\"emo\").upper()         # πάρε τον κωδικό συναισθήματος\n",
    "    \n",
    "    return spk, emo"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "18ce5e2a",
   "metadata": {},
   "source": [
    "Τώρα πάμε να προσπελάσουμε τον κατάλογο της βάσης διαβάζοντας κάθε WAV αρχείο και να χρησιμοποιήσουμε την παραπάνω συνάρτηση για την εξαγωγή πληροφορίας/κατηγοριοποίηση των αρχείων."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5c688d18",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ======= Προσπέλαση βασης ===========\n",
    "\n",
    "# Μια λίστα για να κρατάμε πληροφορίες\n",
    "rows = []\n",
    "\n",
    "for wf in wav_files:                      # Για κάθε όνομα αρχείου στη λίστα wav_files που δημιουργήσαμε στην αρχή, στο πρώτο κελί της άσκησης...\n",
    "    spk, emo = parse_emodb_filename(wf)   # ...βρες και πάρε τον κωδικό ομιλητή και τον κωδικό συναισθήματος από την ονοματοδοσία του αρχείου...\n",
    "    sr, x = wav.read(str(wf))             # ...διάβασε και το WAV αρχείο και αποθήκευσέ το...\n",
    "    n_samples = int(x.shape[0])\n",
    "    dur = n_samples / float(sr)           # ...και υπολόγισε τη διάρκειά του σε δευτερόλεπτα\n",
    "\n",
    "    # Μια λίστα από dictionaries για να κρατάμε πληροφορίες για κάθε αρχείο WAV\n",
    "    rows.append({\n",
    "        \"Αρχείο\": wf.name,\n",
    "        \"Ομιλητής\": spk,\n",
    "        \"Κωδικός συναισθήματος\": emo,\n",
    "        \"Συναίσθημα\": EMO_MAP.get(emo, \"UNKNOWN\"),\n",
    "        \"Fs\": int(sr),\n",
    "        \"Πλήθος δειγμάτων\": n_samples,\n",
    "        \"Διάρκεια (s)\": float(dur),\n",
    "    })"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8d6b8081",
   "metadata": {},
   "source": [
    "Ας τυπώσουμε τι μάθαμε **γενικά** για τη βάση μας! "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "241ecc1f",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ====== Στατιστικά ======\n",
    "speakers = [r[\"Ομιλητής\"] for r in rows]\n",
    "emos = [r[\"Κωδικός συναισθήματος\"] for r in rows]\n",
    "durations = np.array([r[\"Διάρκεια (s)\"] for r in rows], dtype=float)\n",
    "srs = np.array([r[\"Fs\"] for r in rows], dtype=int)\n",
    "\n",
    "print(\"\\n=== Σύνοψη Βάσης Δεδομένων ===\")\n",
    "print(\"Πλήθος ομιλητών:\", len(set(speakers)), sorted(set(speakers)))\n",
    "print(\"Κωδικοί συναισθημάτων:\", sorted(set(emos)))\n",
    "print(\"Ονόματα συναισθημάτων:\", [EMO_MAP.get(c, \"UNKNOWN\") for c in sorted(set(emos))])\n",
    "print(\"Συχνότητα δειγματοληψίας (Hz):\", sorted(set(srs.tolist())))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "50dcd46d",
   "metadata": {},
   "source": [
    "Βλέπουμε λοιπόν - αν τα κάνατε όλα σωστά - ότι έχουμε 10 ομιλητές, 7 συναισθήματα, και όλα τα WAV αρχεία μας έχουν συχνότητα δειγματοληψίας 16000 Hz. Μια χαρά! 😉\n",
    "\n",
    "Πάμε σε λίγο περισσότερο βάθος - μη βιάζεστε να προχωρήσουμε στην αναγνώριση του συναισθήματος! Η κατανόηση των δεδομένων που δουλεύουμε είναι σημαντική!\n",
    "\n",
    "Πώς είναι κατανεμημένα τα WAV αρχεία ανά κλάση/συναίσθημα?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7fd4436c",
   "metadata": {},
   "outputs": [],
   "source": [
    "from collections import Counter\n",
    "\n",
    "cc = Counter(emos) # μετράμε τα συναισθήματα και το πλήθος των WAV ανά συναίσθημα\n",
    "N = sum(cc.values()) # το άθροισμά τους (535)\n",
    "\n",
    "print(\"\\nΠλήθος ηχογραφήσεων ανά κλάση:\")\n",
    "\n",
    "# Ας δούμε την κατανομή!\n",
    "for c, k in sorted(cc.items(), key=lambda kv: (-kv[1], kv[0])):\n",
    "    print(f\"  {c} ({EMO_MAP.get(c,'UNKNOWN'):>8s}): {k:4d}  ({k/N:.2%})\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8857607e",
   "metadata": {},
   "source": [
    "Ενδιαφέρον! Κάθε συναίσθημα δεν έχει το ίδιο πλήθος αρχείων! Υπάρχει μια ανισοκατανομή στη *συγκεκριμένη* βάση δεδομένων ανά συναίσθημα. Αυτό είναι κάτι που πρέπει να γνωρίζουμε."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "91758bf5",
   "metadata": {},
   "source": [
    "Ας δούμε τη διάρκεια σε δευτερόλεπτα κάθε αρχείου ανά κλάση/συναίσθημα."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a3722f08",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"\\nΔιάρκεια ανά κλάση (δευτερόλεπτα):\")\n",
    "for c in sorted(set(emos)):\n",
    "    d = np.array([r[\"Διάρκεια (s)\"] for r in rows if r[\"Κωδικός συναισθήματος\"] == c], dtype=float)\n",
    "    print(f\"  {c} ({EMO_MAP.get(c,'UNKNOWN'):>8s}): n={d.size:3d} \"\n",
    "          f\"Μέσος όρος={d.mean():.3f}, Τυπ. απόκλιση={d.std():.3f}, Μεσαίος={np.median(d):.3f}, \"\n",
    "          f\"Ελάχιστο={d.min():.3f}, Μέγιστο={d.max():.3f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "37bed073",
   "metadata": {},
   "source": [
    "Κι εδώ βλέπουμε ενδιαφέροντα πράγματα! Όμως είναι δύσκολο να αποκωδικοποιήσουμε τα νούμερα έτσι όπως παρουσιάζονται. Δε θα ήταν πιο εύκολο να τα οπτικοποιήσουμε?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c31318c7",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Πόσα συναισθήματα έχουμε?\n",
    "classes = sorted(set(emos))\n",
    "\n",
    "# Ας μαζέψουμε τις διάρκειες ανά κλάση/συναίσθημα\n",
    "data = [np.array([r[\"Διάρκεια (s)\"] for r in rows if r[\"Κωδικός συναισθήματος\"] == c], dtype=float)\n",
    "        for c in classes]\n",
    "\n",
    "# Οπτικοποίηση με violin plot\n",
    "plt.figure(figsize=(8, 4))\n",
    "vp = plt.violinplot(data, showmeans=False, showmedians=True, showextrema=True)\n",
    "\n",
    "# Χρώματα από colormap (π.χ. tab10 / Set2 / viridis)\n",
    "cmap = plt.cm.Set2\n",
    "colors = cmap(np.linspace(0, 1, len(classes)))\n",
    "\n",
    "for body, col in zip(vp[\"bodies\"], colors):\n",
    "    body.set_facecolor(col)\n",
    "    body.set_edgecolor(\"black\")\n",
    "    body.set_alpha(0.85)\n",
    "\n",
    "# Γραμμές (median/extrema) – εδώ τις κάνουμε ίδιες για όλα τα σχήματα (π.χ. μαύρες)\n",
    "for k in (\"cmedians\", \"cmins\", \"cmaxes\", \"cbars\"):\n",
    "    if k in vp:\n",
    "        vp[k].set_color(\"black\")\n",
    "        vp[k].set_linewidth(1.2)\n",
    "\n",
    "# Ομορφαίνουμε\n",
    "plt.xticks(np.arange(1, len(classes)+1), [f\"{c}\\n{EMO_MAP.get(c,'UNKNOWN')}\" for c in classes])\n",
    "plt.ylabel(\"Διάρκεια (s)\")\n",
    "plt.title(\"Κατανομή διάρκειας ανά κλάση\")\n",
    "plt.tight_layout()\n",
    "plt.grid()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1bf43dca",
   "metadata": {},
   "source": [
    "### Ερώτηση: \n",
    "\n",
    "**Σε αυτή ΚΑΙ ΜΟΝΟ ΑΥΤΗ τη βάση δεδομένων**, τι καταλαβαίνουμε για την κατανομή της διάρκειας των συναισθημάτων? Γράψτε παρακάτω τη σκέψη σας."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c0fd12e5",
   "metadata": {},
   "source": []
  },
  {
   "cell_type": "markdown",
   "id": "e274bb72",
   "metadata": {},
   "source": [
    "Κλείνοντας αυτήν την περιήγηση στα δεδομένα μας, ας δούμε και πως κατανέμονται οι ομιλητές ανά συναίσθημα."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "310524f5",
   "metadata": {},
   "outputs": [],
   "source": [
    "from collections import Counter, defaultdict\n",
    "\n",
    "# ====== Ομιλητής x Κλάση ======\n",
    "spk_class = defaultdict(Counter)\n",
    "for r in rows:\n",
    "    spk_class[r[\"Ομιλητής\"]][r[\"Κωδικός συναισθήματος\"]] += 1\n",
    "\n",
    "classes_sorted = sorted(set(emos))\n",
    "header = \"Ομιλητής \" + \" \".join([f\"{c:>6s}\" for c in classes_sorted]) + \" | Σύνολο\"\n",
    "\n",
    "print(\"\\nΟμιλητής × Κατανομή κλάσεων:\")\n",
    "\n",
    "print(header)\n",
    "\n",
    "print(\"-\" * len(header))\n",
    "for spk in sorted(spk_class.keys()):\n",
    "    total = sum(spk_class[spk].values())\n",
    "    row = f\"{spk:>7s} \" + \" \".join([f\"{spk_class[spk].get(c,0):6d}\" for c in classes_sorted]) + f\"  |{total:5d}\"\n",
    "    print(row)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "88ab92c1",
   "metadata": {},
   "source": [
    "Εδώ παρατηρούμε ότι ο ομιλητής $16$ έχει δώσει τα περισσότερα συνολικά δείγματα ($71$), ενώ ο ομιλητής $12$ τα λιγότερα ($35$). Επίσης, ο ομιλητής $08$ δεν έχει δώσει *κανένα* δείγμα από το συναίσθημα της Αηδίας (Ε), ενώ ο ομιλητής $14$ έχει δώσει τα περισσότερα δείγματα σε οποιαδήποτε κλάση, με $16$ στην κλάση του Θυμού (W). Τέλος, όλοι οι ομιλητές έδωσαν *πολλά* δείγματα στην κλάση του Θυμού, γι' αυτό και έχουμε ανισορροπία στις κλάσεις, με το Θυμό να έχει $127$ συνολικά δείγματα.\n",
    "\n",
    "Ώρα είναι να ακούσουμε και μερικά τυχαία δείγματα!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ca9dadae",
   "metadata": {},
   "outputs": [],
   "source": [
    "rng = np.random.default_rng(1337)\n",
    "\n",
    "r0 = rows[0]\n",
    "\n",
    "def get_key(d, options):\n",
    "    for k in options:\n",
    "        if k in d:\n",
    "            return k\n",
    "\n",
    "k_emo = get_key(r0, [\"Κωδικός συναισθήματος\"])\n",
    "k_file = get_key(r0, [\"Αρχείο\", \"Όνομα αρχείου\"])\n",
    "\n",
    "def to_path(v):\n",
    "    p = Path(v)\n",
    "    return p if p.is_absolute() else (DATA_DIR / p)\n",
    "\n",
    "classes = sorted({r[k_emo] for r in rows})\n",
    "\n",
    "for c in classes:\n",
    "    subset = [r for r in rows if r[k_emo] == c]\n",
    "    rep = subset[int(rng.integers(0, len(subset)))]\n",
    "    wav_path = to_path(rep[k_file])\n",
    "    emo_name = EMO_MAP.get(c, c) if \"EMO_MAP\" in globals() else str(c)\n",
    "    display(Markdown(f\"### {c} — {emo_name}\\n`{wav_path.name}`\"))\n",
    "    display(Audio(filename=str(wav_path)))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "67b081f6",
   "metadata": {},
   "source": [
    "Ακόμα κι αν δεν ξέρετε Γερμανικά, ο εγκέφαλός σας πρέπει να καταλαβαίνει το φερόμενο συναίσθημα. Και πόσο εύκολα το κάνει!... 😯\n",
    "\n",
    "Παρ' όλο που υπάρχουν μέθοδοι που μπορούν να βγάλουν καλά αποτελέσματα, εσείς δεν τις γνωρίζετε και επίσης δε γνωρίζετε τα χαρακτηριστικά που πρέπει να εξάγετε. \n",
    "\n",
    "Ας απλοποιήσουμε το πρόβλημα λοιπόν. Ας κρατήσουμε μόνο **ΤΡΙΑ** ($3$) συναισθήματα: \n",
    "\n",
    "#### *Θυμό, Χαρά, και Ουδέτερο* \n",
    "\n",
    "για το προβλημά μας."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b32e2331",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Λίστες για να κρατήσουμε μόνο αυτά που είπαμε\n",
    "speaker_ids = []\n",
    "emotion_codes = []\n",
    "kept_files = []\n",
    "\n",
    "# Επαναλαμβάνουμε το ίδιο με πριν\n",
    "for wf in wav_files:\n",
    "    spk, emo = parse_emodb_filename(wf)\n",
    "    speaker_ids.append(spk)\n",
    "    emotion_codes.append(emo)\n",
    "    kept_files.append(wf)\n",
    "\n",
    "# Κρατάμε μόνο ΤΡΙΑ συναισθήματα - Θυμός, Ουδέτερο, Χαρά\n",
    "KEEP = {\"W\", \"N\", \"F\"}\n",
    "\n",
    "# Κρατάμε μόνο ομιλητές, αρχεία, και κωδικούς συναισθημάτων που αφορούν τα τρια παραπάνω συναισθήματα\n",
    "filtered = [(wf, spk, emo) for wf, spk, emo in zip(kept_files, speaker_ids, emotion_codes) if emo in KEEP]\n",
    "kept_files, speaker_ids, emotion_codes = map(list, zip(*filtered)) if filtered else ([], [], [])\n",
    "\n",
    "print(f\"Κρατήσαμε: {len(kept_files)} files\")\n",
    "print(\"Συναισθήματα και πλήθος WAV σε καθένα:\", Counter(emotion_codes))\n",
    "print(\"Ομιλητές:\", sorted(set(speaker_ids)))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "eddf405c",
   "metadata": {},
   "source": [
    "- **Εξαγωγή χαρακτηριστικών**: \n",
    "\n",
    "Παρ' όλο που υπάρχουν ΠΑΡΑ ΠΟΛΛΑ χαρακτηριστικά που μπορεί κανείς να εξάγει από την ομιλία, εμείς πάλι θα χρησιμοποιήσουμε την **ενέργεια** (μόνο!) κάθε σήματος, υπολογισμένη ανά παράθυρο (frame), όπως πριν. Αυτή τη φορά όμως θα το κάνουμε με μια μικρούλα παραλλαγή. Θα υπολογίσουμε την **Root Mean-Square Ενέργεια** η οποία δίνεται ως \n",
    "\n",
    "$$RMS_{energy} = \\sqrt{\\frac{1}{N}\\sum_{i=1}^N x^2[n]}$$\n",
    "\n",
    "για κάθε στοιχείο του διανύσματος $x$ που αντιπροσωπεύει ένα frame, και η οποία έχει κάμποσα πλεονεκτήματα σε σχέση με την \"απλή\" ενέργεια, ειδικά όταν θέλουμε να χτίσουμε αλγορίθμους που \"μαθαίνουν\" από αυτή.\n",
    "\n",
    "$\\underline{\\textrm{Σημείωση:}}$ Στο συνεχή χρόνο που εξετάζουμε στο μάθημα, η ενέργεια αυτή θα γραφόταν ως\n",
    "\n",
    "$$RMS_{energy} = \\sqrt{\\frac{1}{T}\\int_{t_0}^{t_0+T}x^2(t)dt}$$\n",
    "\n",
    "Υλοποιήστε παρακάτω μια συνάρτηση που υπολογίζει την RMS ενέργεια - όπως τη βλέπετε πιο πάνω, με το άθροισμα - για ένα frame ομιλίας (**εδώ πρέπει να συμπληρώσετε κώδικα**)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "292c58d2",
   "metadata": {},
   "outputs": [],
   "source": [
    "def rms_energy(frame):\n",
    "    N = len(frame)\n",
    "    under_root = # INSERT CODE HERE, χρησιμοποιήστε τη np.sum και το σύμβολο ** για την ύψωση σε δύναμη,\n",
    "                 # και προσθέστε axis=1 σαν τελευταίο όρισμα της np.sum\n",
    "                 \n",
    "    return np.sqrt(under_root + 1e-12)\n",
    "\n",
    "# Test:\n",
    "test_signal = np.asarray([[1.1, 2.2], [-1, -9]])\n",
    "print(rms_energy(test_signal)) # πρέπει να πάρετε [1.73925271 6.40312424]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dbe392f5",
   "metadata": {},
   "source": [
    "Νωρίτερα, στον ανιχνευτή ομιλίας με βάση την ενέργεια, χρησιμοποιήσαμε την ενέργεια ανά frame *χωρίς επικάλυψη* των γειτονικών frames. Όμως στην πραγματικότητα, κάποια επικάλυψη είναι πολλές φορές χρήσιμη για να παρακολουθούμε στενότερα την εξέλιξη της τιμής της ενέργειας προϊόντος του χρόνου. Γι' αυτό λοιπόν θα υιοθετήσουμε μια τέτοια προσέγγιση, όπου θα υπάρχει επικάλυψη κατά $50\\%$ μεταξύ των γειτονικών frames. \n",
    "\n",
    "Σας δίνεται παρακάτω έτοιμη συνάρτηση που σας επιστρέφει ένα διδιάστατο πίνακα, όπου σε κάθε γραμμή του έχετε κι ένα frame διάρκειας $30$ ms, με επικάλυψη $15$ ms με το προηγούμενό του."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0a47fb63",
   "metadata": {},
   "outputs": [],
   "source": [
    "def frame_signal(x, frame_len, hop):\n",
    "    \"\"\"\n",
    "    Επιστρέφει έναν 2Δ πίνακα (n_frames, frame_len) που σε κάθε γραμμή του έχει ένα frame διάρκειας \n",
    "    frame_len δειγμάτων του συνολικού σήματος φωνής.\n",
    "    \n",
    "    Αν τα frames που θέλουμε δε \"χωράνε\" στο συνολικό σήμα, γίνεται ένα μικρό padding στο τέλος.\n",
    "    \"\"\"\n",
    "    if len(x) < frame_len:\n",
    "        x = np.pad(x, (0, frame_len - len(x)), mode=\"reflect\")\n",
    "\n",
    "    n_frames = 1 + int(np.ceil((len(x) - frame_len) / hop))\n",
    "    total_len = (n_frames - 1) * hop + frame_len\n",
    "    if total_len > len(x):\n",
    "        x = np.pad(x, (0, total_len - len(x)), mode=\"reflect\")\n",
    "\n",
    "    shape = (n_frames, frame_len)\n",
    "    strides = (x.strides[0] * hop, x.strides[0])\n",
    "    \n",
    "    return np.lib.stride_tricks.as_strided(x, shape=shape, strides=strides)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "97c40b76",
   "metadata": {},
   "source": [
    "Αν λοιπόν καλέσουμε\n",
    "\n",
    "```E = rms_energy(frame_signal(x, ...))```\n",
    "\n",
    "θα πάρουμε ένα διάνυσμα ```Ε``` που θα έχει μια τιμή ενέργειας για κάθε frame ενός σήματος ομιλίας, δηλ. $N$ τιμές, όσες και τα frames ενός σήματος ομιλίας!"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8774f4b6",
   "metadata": {},
   "source": [
    "Καταλαβαίνετε όμως οτι\n",
    "\n",
    "- Κάθε σήμα φωνής έχει τη δική του διάρκεια, δηλ. το πλήθος των frames του δεν είναι σταθερό.\n",
    "\n",
    "- Το να χρησιμοποιήσουμε όλες τις τιμές ενέργειας για κάθε σήμα φωνής δε μας βοηθά. Πολλές τιμές, με μεγάλη συσχέτιση μεταξύ τους (πόση διαφορά να έχει η ενέργεια μεταξύ δυο γειτονικών frames ???).\n",
    "\n",
    "Θα θέλαμε να **συνοψίσουμε στατιστικά** την \"καμπύλη\" ενέργειας για κάθε σήμα φωνής. Έτσι θα φτιάξουμε ένα νέο, *μικρότερο* διάνυσμα που περιγράφει στατιστικά το σήμα φωνής μέσω των χαρακτηριστικών της ενέργειάς του!"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "366a678e",
   "metadata": {},
   "source": [
    "Πώς θα μπορούσαμε να περιγράψουμε στατιστικά τις τιμές της ενέργειας ενός σήματος? \n",
    "\n",
    "- Η **μέση τιμή** των τιμών της ενέργειας θα ήταν μια καλή ιδέα. Αρκεί? Μάλλον όχι.\n",
    "\n",
    "- Αν προσθέταμε και την **τυπική απόκλιση** των τιμών της ενέργειας του σήματος? Κάπως καλύτερα... τι άλλο μπορούμε να βάλουμε? \n",
    "\n",
    "- Τη **μεσαία τιμή** (median) επίσης. \n",
    "\n",
    "Χμμμ... φτάνουν? Μπορεί, μπορεί και όχι. Ας προσθέσουμε μερικά ακόμα:\n",
    "\n",
    "- **Ασυμμετρία (skewness)**: μετρά από ποιά μεριά βρίσκεται η \"ουρά\" της κατανομής των τιμών της ενέργειας. Θετική ασσυμετρία σημαίνει ότι η \"ουρά\" της κατανομής βρίσκεται προς τα δεξιά.\n",
    "\n",
    "- **Κυρτότητα (kurtosis)**: περιγράφει ένα μέτρο που αφορά το σχήμα των \"ουρών\" της κατανομής σε σχέση με το συνολικό της σχήμα. Υψηλή κυρτότητα σημαίνει ότι οι \"ουρές\" της κατανομής είναι έντονες, υπάρχουν αρκετές τιμές στα άκρα της κατανομής.\n",
    "\n",
    "Μέχρι τώρα έχουμε πέντε ($5$) στατιστικές τιμές ανά \"καμπύλη\" ενέργειας (δηλ. ανά WAV σήμα φωνής). Ούτε πολλές, ούτε λίγες. Υπάρχει ακόμα μια γνωστή τετράδα στατιστικών. Ας τη βάλουμε κι αυτή.\n",
    "\n",
    "- **p10 (10ο εκατοστημόριο)**: Η τιμή κάτω από την οποία βρίσκεται το $10\\%$ των τιμών της ενέργειας (δηλαδή \"οι πιο χαμηλές τιμές\").\n",
    "\n",
    "- **q25 (25ο εκατοστημόριο)**: Η τιμή κάτω από την οποία βρίσκεται το $25\\%$ των τιμών της ενέργειας.\n",
    "\n",
    "- **q75 (75ο εκατοστημόριο)**: Η τιμή κάτω από την οποία βρίσκεται το $75\\%$ των τιμών της ενέργειας.\n",
    "\n",
    "- **p90 (90ο εκατοστημόριο)**: Η τιμή κάτω από την οποία βρίσκεται το $90\\%$ των τιμών της ενέργειας (δηλαδή \"οι πιο υψηλές τιμές\").\n",
    "\n",
    "Παρακάτω σας δίνεται μια συνάρτηση που υπολογίζει όλα αυτά τα στατιστικά από τις τιμές της ενέργειας ενός σήματος φωνής (**εδώ πρέπει να συμπληρώσετε κώδικα**)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b043fe88",
   "metadata": {},
   "outputs": [],
   "source": [
    "def summarize_stats(energy_vector):\n",
    "    \"\"\"\n",
    "    Στατιστικά πάνω σε ένα διάνυσμα εισόδου energy_vector\n",
    "\n",
    "    Επιστρέφει, με τη σειρά:\n",
    "      mean, std, skewness, kurtosis, median, p10, q25, q75, p90\n",
    "    \"\"\"\n",
    "    ev = np.asarray(energy_vector, dtype=float).ravel()\n",
    "    n = ev.size\n",
    "    if n == 0:\n",
    "        return np.zeros(10, dtype=float)\n",
    "\n",
    "    mu = # INSERT CODE HERE  # Κάντε χρήση της ev και της numpy function για τη μέση τιμή (mean) - κάντε cast σε float στο τέλος\n",
    "    sd = # INSERT CODE HERE  # Κάντε χρήση της ev και της numpy function για την τυπική απόκλιση (standard deviation) - κάντε cast σε float στο τέλος\n",
    "    med = # INSERT CODE HERE # Κάντε χρήση της ev και της numpy function για το μεσαίο όρο (median) - κάντε cast σε float στο τέλος\n",
    "\n",
    "    p10, q25, q75, p90 = np.percentile(ev, [10, 25, 75, 90]) # τα εκατοστημόρια\n",
    "\n",
    "    if sd > 1e-12:\n",
    "        sk = float(skew(ev, bias=False))                     # Ασυμμετρία\n",
    "        ku = float(kurtosis(ev, fisher=True, bias=False))    # Κύρτωση\n",
    "    else:\n",
    "        sk, ku = 0.0, 0.0                                    # ...σε περίπτωση που δεν ορίζονται καλώς\n",
    "\n",
    "    return np.array([mu, sd, sk, ku, med, float(p10), float(q25), float(q75), float(p90)], dtype=float)\n",
    "\n",
    "# Ας κωδικοποιήσουμε τα ονόματα για αυτά τα 9 χαρακτηριστικά\n",
    "FUNC_NAMES = [\"mean\", \"std\", \"skew\", \"kurt\", \"median\", \"p10\", \"q25\", \"q75\", \"p90\"]\n",
    "feature_names = [f\"E_{s}\" for s in FUNC_NAMES]\n",
    "print(\"n_features =\", len(feature_names), feature_names)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cc32220f",
   "metadata": {},
   "source": [
    "Ας φτιάξουμε μια συνάρτηση ```extract_features``` που να δέχεται ένα path για ένα σήμα φωνής και να εξάγει αρχικά την ενέργεια, frame-by-frame, και στη συνέχεια τα παραπάνω $9$ χαρακτηριστικά! (**Εδώ πρέπει να συμπληρώσετε κώδικα**)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1a545edb",
   "metadata": {},
   "outputs": [],
   "source": [
    "def extract_features(wav_path, frame_ms=30.0, hop_ms=15.0):\n",
    "    \"\"\"\n",
    "    Εξάγει την RMS ενέργεια από ένα σήμα φωνής και μετά τα \n",
    "    απαιτούμενα 9 χαρακτηριστικά από την ενέργεια αυτή.\n",
    "\n",
    "    Επιστρέφει τα 9 στατιστικά.\n",
    "    \"\"\"\n",
    "    sr, x = wav.read(wav_path)  # Διαβάζουμε το σήμα φωνής\n",
    "    x = np.asarray(x)           # Safety :)\n",
    "\n",
    "    # Κανονικοποίηση\n",
    "    info = np.iinfo(x.dtype)\n",
    "    denom = float(max(abs(info.min), abs(info.max)))\n",
    "    x = x.astype(np.float32) / denom\n",
    "    x = np.clip(x, -1.0, 1.0)\n",
    "    \n",
    "    # Παράμετροι για τη frame-by-frame ανάλυση\n",
    "    frame_len = int(round(sr * frame_ms / 1000.0)) # Μετατρέπουμε τα 30 ms σε δείγματα: 480\n",
    "    hop = int(round(sr * hop_ms / 1000.0))         # Μετατρέπουμε τα 15 ms σε δείγματα: 240\n",
    "    frames = frame_signal(x, frame_len=frame_len, hop=hop) # Καλούμε την frame_signal για την τμηματοποίηση του σήματος\n",
    "    \n",
    "    e = # INSERT CODE HERE # Υπολογίζουμε την ενέργεια για όλα τα frames καλώντας κατάλληλη συνάρτηση\n",
    "\n",
    "    e_stats = # INSERT CODE HERE # Επιστρέφουμε τα στατιστικά για την \"καμπύλη\" ενέργειας καλώντας κατάλληλη συνάρτηση \n",
    "    return e_stats\n",
    "\n",
    "# Testing \n",
    "f0 = extract_features(kept_files[0])\n",
    "print(f0) # Πρέπει να πάρετε [4.50768153e-02 5.79770227e-02 2.05577052e+00 5.07799827e+00\n",
    "          #                   2.35119592e-02 9.99999997e-07 1.72353094e-03 6.95751421e-02 1.08393940e-01]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5f28dd9c",
   "metadata": {},
   "source": [
    "- **Κατασκευή χαρακτηριστικών για όλη τη βάση δεδομένων**:\n",
    "\n",
    "Όλα τώρα είναι έτοιμα! Πρέπει να κάνουμε την παραπάνω διαδικασία για ΚΑΘΕ σήμα φωνής στη βάση. Μαζί με τα $9$ στατιστικά χαρακτηριστικά, πρέπει να κρατήσουμε:\n",
    "\n",
    "1. Τον κωδικό συναισθήματος που αντιστοιχεί σε κάθε WAV (που όμως πλέον κωδικοποιείται όπως είπαμε από τα $9$ στατιστικά)\n",
    "\n",
    "2. Τον κωδικό ομιλητή (το γιατί θα το καταλάβετε σε λίγο)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "bb80a2bd",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Λίστες που μαζεύουμε τα δεδομένα μας \n",
    "data = []      # εδώ μαζεύουμε τα 9 χαρακτηριστικά για κάθε πρόταση\n",
    "label = []     # εδώ μαζεύουμε τον κωδικό συναισθήματος για κάθε πρόταση - είναι το ground truth για κάθε πρόταση\n",
    "speakers = []    # εδώ μαζεύουμε τον κωδικό ομιλητή για κάθε πρόταση\n",
    "\n",
    "# Ξεκινάμε!\n",
    "for wf, spk, emo in zip(kept_files, speaker_ids, emotion_codes):\n",
    "        data.append(extract_features(wf)) # αποθηκεύουμε τα 9 χαρακτηριστικά για το σήμα στο path wf\n",
    "        label.append(emo)                 # ...το label (συναίσθημα) του ...\n",
    "        speakers.append(spk)              # ...και τον κωδικό ομιλητή του\n",
    "\n",
    "# Από λίστες, τα κάνουμε numpy arrays\n",
    "data = np.asarray(data, dtype=float)\n",
    "label = np.asarray(label)\n",
    "speakers = np.asarray(speakers)\n",
    "\n",
    "# Απλό τσεκάρισμα ότι όλα είναι καλά\n",
    "print(\"Data shape:\", data.shape)                    # [277 προτάσεις x 9 χαρακτηριστικά]\n",
    "print(\"Μοναδικά συναισθήματα:\", sorted(set(label))) # 3 συναισθήματα\n",
    "print(\"Μοναδικοί ομιλητές:\", sorted(set(speakers))) # 10 ομιλητές"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2e527308",
   "metadata": {},
   "source": [
    "- **Διαχωρισμός βάσης σε train & test**:\n",
    "\n",
    "Έχουμε λοιπόν στα ```data, label, speakers``` ΟΛΑ ΟΣΑ θέλουμε!\n",
    "\n",
    "Τώρα πρέπει να τα χωρίσουμε σε **δυο ξένα σύνολα**, ένα για να **εκπαιδεύσουμε ένα μοντέλο μηχανικής μάθησης** (training set) κι ένα για να **ελέγξουμε τις *επιδόσεις* του μοντέλου** (test set).\n",
    "\n",
    "Ο διαχωρισμός αυτός δεν μπορεί/δεν πρέπει να είναι εντελώς τυχαίος! Πρέπει \n",
    "\n",
    "(α) ο τυχαίος διαχωρισμός να αντικατοπτρίζει την κατανομή των συναισθημάτων της βάσης στα δυο υποσύνολα training και test.\n",
    "\n",
    "(β) ο τυχαίος διαχωρισμός να σέβεται το γεγονός ότι κάθε ομιλητής πρέπει να βρίσκεται είτε στο training set είτε στο test set! Αν κάποια δείγματα κάποιου ομιλητή βρίσκονται και στα δυο set, η απόδοση της αναγνώρισης του συναισθήματος είναι πολύ καλύτερη από την πραγματικότητα γιατί το μοντέλο έχει \"δει\" στην εκπαίδευσή του κάποιον ομιλητή που βρίσκεται στο, υποτίθεται άγνωστο, test set.\n",
    "\n",
    "Ευτυχώς η ```Python``` έχει έτοιμη συνάρτηση που κάνει αυτό το διαχωρισμό με βάση τη μεταβλητή ```speakers``` που έχουμε δημιουργήσει, την ```StratifiedGroupKFold``` του πακέτου ```scikit-learn```!\n",
    "\n",
    "Απλά τρέξτε το παρακάτω κελί, μη δώσετε σημασία στις λεπτομέρειες. Η ουσία είναι ότι η συνάρτηση αυτή δημιουργεί δυο **δείκτες** στο αρχικό σύνολο των δεδομένων μας, που σέβονται τις δυο παραπάνω προϋποθέσεις."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a2260c5b",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.model_selection import StratifiedGroupKFold\n",
    "\n",
    "# Χωρίζουμε τα δυο σετ, training & test, με βάση τους speakers και την αρχική κατανομή συναισθημάτων της βάσης\n",
    "def stratified_group_split(labels, speakers, seed=1337):\n",
    "    n = len(labels)\n",
    "\n",
    "    sgkf = StratifiedGroupKFold(n_splits=5, shuffle=True, random_state=seed)\n",
    "    \n",
    "    for tr, te in sgkf.split(np.zeros(n), labels, speakers):\n",
    "        return np.asarray(tr), np.asarray(te)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "aed94d65",
   "metadata": {},
   "source": [
    "Η συνάρτηση αυτή επιστρέφει δυο πίνακες: \n",
    "\n",
    "1. Έναν που περιέχει τους **δείκτες** στους πίνακες ```data```, ```label```, ```speakers``` που αντιστοιχούν στο training set. Αυτό αποτελεί περίπου το $80\\%$ της επιλεγμένης βάσης.\n",
    "\n",
    "2. Έναν που περιέχει τους **δείκτες** στους πίνακες ```data```, ```label```, ```speakers``` που αντιστοιχούν στο test set. Αυτό αποτελεί περίπου το $20\\%$ της επιλεγμένης βάσης.\n",
    "\n",
    "Ας τη δοκιμάσουμε!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "93a912dc",
   "metadata": {},
   "outputs": [],
   "source": [
    "train_idx, test_idx = stratified_group_split(label, speakers, seed=RNG_SEED)\n",
    "\n",
    "print(f\"Μέγεθος training set: {len(train_idx)}, Μέγεθος test set: {len(test_idx)}\\n\")\n",
    "print(\"Πλήθος ομιλητών στο training set:\", sorted(set(speakers[train_idx])))\n",
    "print(\"Πλήθος ομιλητών στο test set: \", sorted(set(speakers[test_idx])))\n",
    "\n",
    "# Ας δούμε το ποσοστό αναλογίας κάθε συναισθήματος σε κάθε σετ (train & test)\n",
    "def show_props(idx, name):\n",
    "    labs = sorted(set(label))\n",
    "    props = {lab: float(np.mean(label[idx] == lab)) for lab in labs}\n",
    "    print(name, props)\n",
    "\n",
    "# Ένα cross-check\n",
    "show_props(np.arange(len(label)), \"\\nΣυνολική βάση:\")\n",
    "show_props(train_idx, \"Training  \")\n",
    "show_props(test_idx,  \"Test   \")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e0529585",
   "metadata": {},
   "source": [
    "Βλέπετε ότι οι αναλογίες ανά συναίσθημα είναι κοντά στην αρχική αναλογία της βάσης, δεδομένης και της κατανομής των ομιλητών ανά συναίσθημα: \n",
    "\n",
    "Συνολικη βάση: {'F': $25\\%$, 'N': $28\\%$, 'W': $45\\%$}\n",
    "\n",
    "Training:      {'F': $26\\%$, 'N': $27\\%$, 'W': $46\\%$}\n",
    "\n",
    "Test:          {'F': $22\\%$, 'N': $33\\%$, 'W': $44\\%$} "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "47d767fe",
   "metadata": {},
   "source": [
    "- **Εκπαίδευση Μοντέλου Μηχανικής Μάθησης**:\n",
    "\n",
    "Φτάσαμε στο επιθυμητό σημείο. Τώρα πρέπει απλά να επιλέξουμε ένα μοντέλο μηχανικής μάθησης για να το εκπαιδεύσουμε με τα χαρακτηριστικά που φτιάξαμε: τα $9$ στατιστικά κάθε πρότασης, μαζί με τον κωδικό συναισθήματος, ώστε να μάθει την αντιστοιχία \n",
    "\n",
    "<p style=\"text-align:center;\">\n",
    "τα 9 χαρακτηριστικά της i-οστής πρότασης αντιστοιχούν στο j-οστό συναίσθημα\n",
    "</p>\n",
    "\n",
    "Από τα πραγματικά ΠΑΡΑ ΠΟΛΛΑ μοντέλα, θα επιλέξουμε ένα πολύ απλό. Το μοντέλο της **logistic regression για περισσότερες από δυο κλάσεις (multinomial logistic regression)**.\n",
    "\n",
    "Με *πολύ απλά λόγια*, η multinomial logistic regression είναι ένας απλός ταξινομητής για πολλές κλάσεις: για κάθε κλάση $k$ υπολογίζει ένα \"σκορ\" ως γραμμικό συνδυασμό των χαρακτηριστικών $x$ (των στατιστικών ενέργειας, στη δική μας περίπτωση):\n",
    "\n",
    "$$s_k = w_k^\\top x + b_k$$\n",
    "\n",
    "Μετά μετατρέπει όλα αυτά τα σκορ σε πιθανότητες που αθροίζουν στο 1 με τη συνάρτηση softmax: \n",
    "\n",
    "$$p_k = \\frac{e^{s_k}}{\\sum_j e^{s_j}}$$\n",
    "\n",
    "Στην εκπαίδευση, το μοντέλο αλλάζει τα $w_k, b_k$ ώστε να δίνει όσο γίνεται μεγαλύτερη πιθανότητα στη σωστή κλάση (ελαχιστοποιώντας κάποια συνάρτηση κόστους). Στην πρόβλεψη, απλώς διαλέγει την κλάση με τη μεγαλύτερη πιθανότητα.\n",
    "\n",
    "Όλα αυτά θα τα μάθετε με λεπτομέρεια σε μαθήματα Μηχανικής Μάθησης, και δεν αποτελεί βασικό στοιχείο αυτού του εργαστηρίου. \n",
    "\n",
    "Η βιβλιοθήκη ```scikit-learn``` έχει τα πάντα που θα χρειαστούμε. Απλά τρέξτε τον παρακάτω κώδικα που εκτελεί την εκπαίδευση και τον έλεγχο."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7312206f",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.pipeline import Pipeline\n",
    "from sklearn.linear_model import LogisticRegression\n",
    "from sklearn.preprocessing import StandardScaler\n",
    "\n",
    "# Ορίζουμε το pipeline της εκπαίδευσης - don't worry about it\n",
    "clf = Pipeline([\n",
    "    (\"scaler\", StandardScaler()),\n",
    "    (\"lr\", LogisticRegression(\n",
    "        max_iter=20000,\n",
    "        solver=\"lbfgs\",\n",
    "        multi_class=\"multinomial\",\n",
    "        random_state=RNG_SEED,\n",
    "        class_weight=\"balanced\",\n",
    "        penalty=\"l2\"\n",
    "    ))])\n",
    "\n",
    "# Επιλέγουμε το training και το test set με βάση τους δείκτες που πήραμε από την StratifiedGroupKFold νωρίτερα \n",
    "X_train, y_train = data[train_idx], label[train_idx]\n",
    "X_test, y_test = data[test_idx], label[test_idx]\n",
    "\n",
    "# Εκπαιδεύουμε τον αλγόριθμο!\n",
    "clf.fit(X_train, y_train)\n",
    "\n",
    "# Του ζητάμε να προβλέψει τα συναισθήματα στο test set!\n",
    "y_pred = clf.predict(X_test)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8dfa72d8",
   "metadata": {},
   "source": [
    "Στο διάνυσμα ```y_pred``` υπάρχουν τα labels των συναισθημάτων που πρόβλεψε το μοντέλο για τις προτάσεις που \"επιλέξαμε\" να βρίσκονται στο test set!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fb46df1a",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"Προβλέψεις:\", y_pred)   # Προβλέψεις\n",
    "print(\"Ground truth:\", y_test) # Πραγματικά συναισθήματα (labels)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "16002b79",
   "metadata": {},
   "source": [
    "Δεν είναι καλή ιδέα να κοιτάξουμε \"με το μάτι\" μια προς μια τις τιμές των δυο διανυσμάτων.\n",
    "\n",
    "Ας αξιολογήσουμε αυτή του την προσπάθεια τυπώνοντας τον περίφημο **confusion matrix** (πίνακα σύγχυσης, αλλά δε μου αρέσει η Ελληνική μετάφραση 😄), όπου μας λέει πώς κατανεμήθηκαν οι προβλέψεις του μοντέλου με βάση την πραγματικότητα (τα πραγματικά labels του test set, που τα έχουμε διαθέσιμα αλλά φυσικά δεν τα χρησιμοποιήσαμε πουθενά!).\n",
    "\n",
    "Θα τυπώσουμε δυο ίδιους confusion matrices, έναν με απόλυτα νούμερα επιτυχίας και έναν με ποσοστά επιτυχίας."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "526d76f4",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.metrics import ConfusionMatrixDisplay\n",
    "\n",
    "emo_order = [\"W\", \"N\", \"F\"]\n",
    "labels = [e for e in emo_order if e in set(y_test)]\n",
    "display_labels = [f\"{e}\\n{EMO_MAP[e]}\" for e in labels]\n",
    "\n",
    "fig, axes = plt.subplots(1, 2, figsize=(12, 5))\n",
    "\n",
    "ConfusionMatrixDisplay.from_predictions(\n",
    "    y_test, y_pred,\n",
    "    labels=labels,\n",
    "    display_labels=display_labels,\n",
    "    xticks_rotation=45,\n",
    "    normalize=None,\n",
    "    values_format=\"d\",\n",
    "    cmap=\"Blues\",\n",
    "    ax=axes[0]\n",
    ")\n",
    "axes[0].set_title(\"Confusion matrix (απόλυτες τιμές)\")\n",
    "\n",
    "ConfusionMatrixDisplay.from_predictions(\n",
    "    y_test, y_pred,\n",
    "    labels=labels,\n",
    "    display_labels=display_labels,\n",
    "    xticks_rotation=45,\n",
    "    normalize=\"true\",\n",
    "    values_format=\".2f\",\n",
    "    cmap=\"Blues\",\n",
    "    ax=axes[1]\n",
    ")\n",
    "axes[1].set_title(\"Confusion matrix (ποσοστιαία)\")\n",
    "\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "203c2136",
   "metadata": {},
   "source": [
    "### Ερώτηση: \n",
    "\n",
    "Περιγράψτε στο παρακάτω κελι με λόγια το αποτέλεσμα των παραπάνω confusion matrices. Πώς αποδίδει το μοντέλο για κάθε συναίσθημα? Εξηγήστε αναλυτικά."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d86e5ae4",
   "metadata": {},
   "source": []
  },
  {
   "cell_type": "markdown",
   "id": "fc2331be",
   "metadata": {},
   "source": [
    "---\n",
    "---"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3f654d46",
   "metadata": {},
   "source": [
    "### 4η Άσκηση - Ένας Beethoven στο σπίτι σας!"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "45e2ade8",
   "metadata": {},
   "source": [
    "Βασικό στοιχείο του εργαστηριακού κομματιού του μαθήματος αποτελούν τα ηχητικά σήματα, οπότε ας παίξουμε με αυτά απ'το μηδέν αυτή τη φορά!\n",
    "\n",
    "Αν θελήσουμε να δημιουργήσουμε στην `Python` ένα ημιτονοειδές σήμα συχνότητας $f_0=400$ Hz (νότα ΛΑ), διάρκειας $0.5$ sec (δηλ. $500$ ms), με μηδενική φάση μετατόπισης $\\phi$ και μοναδιαίου πλάτους $A=1$, με συχνότητα δειγματοληψίας $f_s=16000$ Hz (που αντιστοιχεί σε καλής ποιότητας ηχογράφηση) θα γράψουμε τις ακόλουθες εντολές:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7f47a31e",
   "metadata": {},
   "outputs": [],
   "source": [
    "fs = 16000    # συχνότητα δειγματοληψίας (fixed)\n",
    "f0 = 400      # συχνότητα ημιτόνου\n",
    "dur = 500     # διάρκεια σε ms\n",
    "A = 1         # πλάτος ημιτόνου\n",
    "t = np.arange(start=0, stop=dur/1000, step = 1/fs)   # άξονας χρόνου\n",
    "signal = A*np.sin(2*np.pi*f0*t)                      # σήμα"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "afa32689",
   "metadata": {},
   "source": [
    "Για να ακούσουμε αυτό που φτιάξαμε, πληκτρολογούμε"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "53683f9c",
   "metadata": {},
   "outputs": [],
   "source": [
    "Audio(signal, rate=fs)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c98732f7",
   "metadata": {},
   "source": [
    "Στην άσκηση αυτή θα συνθέσουμε τμήμα από το πολύ γνωστό κομμάτι του Beethoven, Fur Elise (https://www.youtube.com/watch?v=q9bU12gXUyM), από νότες πλήκτρων πιάνου."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0fc00373",
   "metadata": {},
   "source": [
    "(α') Με χρήση της βιβλιοθήκης `Pandas` μπορούμε να διαβάσουμε ένα CSV αρχείο `FurEliseData.csv` που μας δίνεται"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f8c76947",
   "metadata": {},
   "outputs": [],
   "source": [
    "df = pd.read_csv('FurEliseData.csv')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "99dcab79",
   "metadata": {},
   "source": [
    "ως ένα `DataFrame`, και να το τυπώσουμε."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "be6675d0",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(df)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "26543144",
   "metadata": {},
   "source": [
    "Όπως βλέπετε, υπάρχουν $4$ στήλες, `lefthand, righthand, ldur, rhdur` οι οποίες ανταποκρίνονται σε αριθμούς πλήκτρου πιάνου που παίζονται από το αριστερό χέρι (`lefthand`), με τις αντίστοιχες διάρκειές τους σε ms (`lhdur`), και σε αριθμούς πλήκτρου πιάνου που παίζονται με το δεξί χέρι (`righthand`), με τις αντίστοιχες επίσης διάρκειές τους σε ms (`rhdur`). Τα μηδενικά αντιστοιχούν σε παύσεις (δεν παίζουμε κάποιο πλήκτρο), αντίστοιχης διάρκειας.\n",
    "\n",
    "Αυτούς μπορούμε να τους εισάγουμε σε ξεχωριστούς πίνακες της Python ως εξής:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "931a6394",
   "metadata": {},
   "outputs": [],
   "source": [
    "lefthand = df[\"lefthand\"].values\n",
    "righthand = df[\"righthand\"].values\n",
    "lhdur = df[\"lhdur\"].values\n",
    "rhdur = df[\"rhdur\"].values\n",
    "\n",
    "print(f\"Τα πλήκτρα πιάνου του αριστερού χεριού είναι τα: \\n {lefthand}\")\n",
    "print(f\"Οι διάρκειες σε ms κάθε πλήκτρου του αριστερού χεριού είναι: \\n {lhdur} ms\")\n",
    "\n",
    "print(f\"Τα πλήκτρα πιάνου του δεξιού χεριού είναι τα: \\n {righthand}\")\n",
    "print(f\"Οι διάρκειες σε ms κάθε πλήκτρου του δεξιού χεριού είναι: \\n {rhdur} ms\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8fdf17d7",
   "metadata": {},
   "source": [
    "(β') Μετατρέψτε από κάθε χέρι, κάθε αριθμό πλήκτρου πιάνου $n$ στην αντίστοιχη συνχότητα με τη σχέση\n",
    "<br>\n",
    "$$\\Large f(n) = f_\\mathrm{LA} \\cdot 2^{\\frac{n-49}{12}} = 440 \\cdot 2^{\\frac{n-49}{12}} $$\n",
    "Στην `Python` αυτό γίνεται εύκολα ως"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "370a169f",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Μετατροπή πλήκτρου πιάνου σε νότα για το αριστερό χέρι\n",
    "freq_lh = 440 * (2 ** ((lefthand-49)/12))\n",
    "\n",
    "# Μετατροπή πλήκτρου πιάνου σε νότα για το αριστερό χέρι (συμπληρώστε)\n",
    "freq_rh = # INSERT CODE"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "724a9155",
   "metadata": {},
   "source": [
    "με αποτέλεσμα έναν πίνακα - διάνυσμα `freq` με τις αντίστοιχες συχνότητες. Οι παύσεις αναπαρίστανται στους πίνακες με τα πλήκτρα ως $(0)$. Θα μετατραπούν και αυτές - ενώ δεν πρέπει - όμως αυτό δε θα μας επηρεάσει, καθώς τις παύσεις θα τις χειριστούμε ξεχωριστά όπως θα δείτε παρακάτω."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "39aaefc4",
   "metadata": {},
   "source": [
    "(γ') Οι πίνακες `lhdur, rhdur` περιέχουν τη διάρκεια κάθε νότας σε ms για το αριστερό και το δεξί χέρι, αντίστοιχα. \n",
    "\n",
    "Ακολουθώντας τον παρακάτω κώδικα, δημιουργήστε τα αντίστοιχα ημίτονα με τη σωστή διάρκεια. Για τις παύσεις, θα δημιουργήσουμε ένα μηδενικό διάνυσμα με την ανάλογη διάρκεια."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7a4b7e24",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Πόσα πλήκτρα έχουμε στο αριστερό χέρι?\n",
    "N = # INSERT CODE\n",
    "\n",
    "# Συχνότητα δειγματοληψίας\n",
    "fs = 16000\n",
    "\n",
    "# Μια άδεια λίστα για να μαζεύουμε τα ημίτονα που θα φτιάχνουμε\n",
    "signal_left = []\n",
    "\n",
    "# Ξεκινάμε:\n",
    "for i in range(1, N):\n",
    "    \n",
    "    # Ο άξονας χρόνου για κάθε πλήκτρο:\n",
    "    t = np.arange(start=0, stop=lhdur[i]/1000, step=1/fs)\n",
    "    \n",
    "    if lefthand[i] == 0:\n",
    "        # αν έχουμε παύση, δημιουργύμε ένα μηδενικό διάνυσμα διάρκειας lhdur[i] ms:\n",
    "        signal_left = np.append(signal_left, np.zeros(len(t)))\n",
    "    else:\n",
    "        # φτιάξτε το αντίστοιχο ημίτονο με χρήση του διανύσματος freq_lh (χρησιμοποιήστε την np.sin() και το παράδειγμα στην αρχή της άσκησης)\n",
    "        new_sin = # INSERT CODE\n",
    "        # \"μαζέψτε\" όλα τα ημίτονα στη λίστα signal_left\n",
    "        signal_left = np.append(signal_left, new_sin)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1c49effe",
   "metadata": {},
   "source": [
    "(δ') Η μεταβλητή `signal_left` εδώ δηλώνεται ως μία λίστα (https://www.w3schools.com/python/python_lists.asp) της `Python`, στην οποία μπορούμε να *προσαρτήσουμε* (append) οποιαδήποτε μεταβλητή οποιουδήποτε μεγέθους. Αυτό είναι βολικό γιατί οι νότες δεν έχουν όλες την ίδια διάρκεια, και ένας πίνακας $N\\times L$ θα ήταν δύσχρηστος μιας και το $L$ αλλάζει δυναμικά."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fdf6f2a8",
   "metadata": {},
   "source": [
    "(ε') Επαναλάβετε τη διαδικασία για το δεξί χέρι, δηλ. τις μεταβλητές `righthand, rhdur` και αποθηκεύστε τα αποτελέσματά σας σε ξεχωριστές μεταβλητές. Παρατηρήστε ότι το αριστερό χέρι παίζει λιγότερες νότες από το δεξί."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c70e7237",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Πόσα πλήκτρα έχουμε στο δεξί χέρι?\n",
    "N = # INSERT CODE\n",
    "\n",
    "# Συχνότητα δειγματοληψίας\n",
    "fs = 16000\n",
    "\n",
    "# Μια άδεια λίστα για να μαζεύουμε τα ημίτονα που θα φτιάχνουμε\n",
    "signal_right = []\n",
    "\n",
    "# Ξεκινάμε:\n",
    "for i in range(1, N):\n",
    "    \n",
    "    # Ο άξονας χρόνου για κάθε πλήκτρο:\n",
    "    t = np.arange(start=0, stop=rhdur[i]/1000, step=1/fs)\n",
    "    \n",
    "    if righthand[i] == 0:\n",
    "        # αν έχουμε παύση, δημιουργύμε ένα μηδενικό διάνυσμα διάρκειας rhdur[i] ms:\n",
    "        signal_right = np.append(signal_right, np.zeros(len(t)))\n",
    "    else:\n",
    "        # φτιάξτε το αντίστοιχο ημίτονο με χρήση του διανύσματος freq_rh  (χρησιμοποιήστε την np.sin() και το παράδειγμα στην αρχή της άσκησης)\n",
    "        new_sin = # INSERT CODE\n",
    "        # \"μαζέψτε\" όλα τα ημίτονα στη λίστα signal_right\n",
    "        signal_right = np.append(signal_right, new_sin)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4cc79fa0",
   "metadata": {},
   "source": [
    "(ζ) Δημιουργήστε το συνολικό σήμα, αθροίζοντας τα δύο σήματα ως\n",
    "\n",
    "`sig = signal_left + signal_right`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5a2b8eec",
   "metadata": {},
   "outputs": [],
   "source": [
    "sig = signal_left + signal_right # Διαβάστε παρακάτω αν σας βγάλει error!"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1e5f4a39",
   "metadata": {},
   "source": [
    "Αν τα έχετε κάνει όλα σωστά, η `Python` θα σας επιστρέψει σφάλμα, γιατί το ένα σήμα βγήκε λίγο μικρότερο από το άλλο, οπότε δε γίνεται να προστεθούν (να θυμάστε ότι οι πράξεις μεταξύ πινάκων πρέπει να τηρούν τους κανόνες διάστασης των πινάκων). Χρησιμοποιήστε την εντολή `len` και μετρήστε το μήκος του κάθε σήματος. Πόση είναι η διαφορά τους σε πλήθος τιμών;"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "176bffda",
   "metadata": {},
   "outputs": [],
   "source": [
    "len_left = len(signal_left)\n",
    "print(f\"To μήκος σήματος αριστερού χεριού είναι {len_left} δείγματα.\")\n",
    "\n",
    "len_right = len(signal_right)\n",
    "print(f\"To μήκος σήματος δεξιού χεριού είναι {len_left} δείγματα.\")\n",
    "\n",
    "D = len_right - len_left\n",
    "print(f\"Το σήμα δεξιού χεριού είναι μεγαλύτερο κατά {D} δείγματα.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4ae59d8f",
   "metadata": {},
   "source": [
    "(η') Για μια γρήγορη διόρθωση στο παραπάνω πρόβλημα, χρησιμοποιήστε τις εντολές"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "60b771a4",
   "metadata": {},
   "outputs": [],
   "source": [
    "D = len(signal_right) - len(signal_left)\n",
    "signal_left = np.append(signal_left, np.zeros(D))\n",
    "sig = signal_left + signal_right"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f7ae3981",
   "metadata": {},
   "source": [
    "όπου απλά συμπληρώνουμε με μηδενικά το μικρότερο διάνυσμα για να γίνει ίσου \"μεγέθους\" με το μεγαλύτερο."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4fdb38f3",
   "metadata": {},
   "source": [
    "(θ') Μπορείτε τώρα να ακούσετε το σήμα σας!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8315c6c8",
   "metadata": {},
   "outputs": [],
   "source": [
    "sig = signal_left + signal_right\n",
    "\n",
    "# Ακούμε (χαμηλώστε τον ήχο)\n",
    "Audio(sig, rate=fs)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "18094376",
   "metadata": {},
   "source": [
    "Να σημειωθεί ότι ένα **πραγματικό πλήκτρο πιάνου** δεν παράγει *ένα* ημίτονο, όπως κάναμε εμείς, αλλά ένα **άθροισμα από ημίτονα**, με συχνότητες σχεδόν ακέραιες πολλαπλάσιες της θεμελιώδους συχνότητας και διαφορετικά μεταξύ τους πλάτη. Με άλλα λόγια, παράγει - προσεγγιστικά - **μια σειρά Fourier**, με πεπερασμένο πλήθος όρων, την οποία θα μελετήσουμε σύντομα. Επίσης, σε ένα πραγματικό πιάνο, υπάρχουν πάρα πολλές λεπτομέρειες που μας χαρίζουν τον πλούσιο ήχο του, οι οποίες δεν είναι απλό να μοντελοποιηθούν.\n",
    "\n",
    "$\\underline{\\text{Σημείωση}}$: Δείτε παρακάτω το άρθρο με τίτλο \"Perceptual significance of inharmonicity and spectral envelope in the piano bass range\", των Galembo et al. \n",
    "\n",
    "Πηγαίνετε στη σελίδα $6$, εξίσωση $(7)$, για να καταλάβετε πόσο πολύπολοκο είναι να συνθέσει κανείς έναν ήχο πλήκτρου πιάνου που να μοιάζει με τον πραγματικό. Η απλή όψη της εξίσωσης είναι αρκετή για να σας πείσει! 😉 \n",
    "\n",
    "Link: https://www.researchgate.net/publication/225284530_Perceptual_relevance_of_inharmonicity_and_spectral_envelope_in_the_piano_bass_range"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d8de9bf6",
   "metadata": {},
   "source": [
    "---\n",
    "---"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "99a1a411",
   "metadata": {},
   "source": [
    "### 5η Άσκηση - Ένας πιο εξωτικός Beethoven στο σπίτι σας!"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8b57828f",
   "metadata": {},
   "source": [
    "Όσοι/ες εκτελέσατε με επιτυχία την προηγούμενη άσκηση, θα παρατηρήσατε ότι ο ήχος που δημιουργήσατε είχε κάποια ηχητικά clicks/απότομες παύσεις κάθε ϕορά που άλλαζε μια νότα. \n",
    "\n",
    "Σίγουρα συμφωνείτε ότι είναι πολύ ενοχλητικό στο αυτί μας. "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9ee4f9cd",
   "metadata": {},
   "source": [
    "Ο λόγος εμφάνισης αυτών των clicks είναι ο εξής: όταν δημιουργείτε ένα ημίτονο μιας συγκεκριμένης συχνότητας και συγκεκριμένης διάρκειας, δε σημαίνει απαραίτητα ότι το ημίτονο ξεκινά και τερματίζει σε μηδενικό πλάτος. Ο τερματισμός ενός ημιτόνου σε τυχαίο πλάτος και η αρχή του επομένου από μηδενικό πλάτος δημιουργεί μια απότομη αλλαγή, μια ασυνέχεια/αλλαγή φάσης, η οποία ακούγεται σαν click. Δείτε το Σχήμα παρακάτω. \n",
    "\n",
    "![disc](./discontinuities.png)\n",
    "\n",
    "Σε κάθε παράδειγμα του σχήματος βλέπετε δυο ημίτονα διαφορετικής συχνότητας σαν αυτά που δημιουργήσατε στην προηγούμενη άσκηση, με το ένα να ακολουθεί το άλλο. Αν οι διάρκεια του πρώτου ημιτόνου είναι κατάλληλα επιλεγμένη, τότε η συνένωση της δεύτερης νότας γίνεται ομαλά, όπως στο Σχήμα (α). Όμοια και στο Σχήμα (β), όπου η συνένωση είναι ακόμα πιο ομαλή και μια τέτοια περίπτωση είναι ιδανική. Και στις δυο περιπτώσεις τα clicks είναι ανεπαίσθητα ή ανύπαρκτα. \n",
    "\n",
    "Όμως αυτό που στην πραγματικότητα κάνατε στην προηγούμενη άσκηση είναι το Σχήμα (γ), καθώς η διάρκεια της κάθε νότας δεν τερμάτιζε απαραίτητα σε σημείο τέτοιο ώστε η ένωση της με το ημίτονο της επόμενης νότας να σχηματίζει \"συνέχεια\" στο σήμα. Τις περισσότερες - αν όχι όλες - φορές, ένα ημίτονο που δημιουργούσατε τερμάτιζε σε στιγμή που το πλάτος του δεν ήταν μηδενικό, και αναγκαστικά δημιουργούνταν μια ασυνέχεια στο ηχητικό σήμα λόγω της προσθήκης της επόμενης νότας, που ξεκινούσε από το μηδέν (αφού ήταν συναρτήσεις $\\sin(\\cdot)$, αλλά το ίδιο ϑα συνέβαινε αν χρησιμοποιούσαμε $\\cos(\\cdot))$."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0b938928",
   "metadata": {},
   "source": [
    "Μια εύκολη λύση αυτού του προβλήματος είναι ο πολλαπλασιασμός του ημιτόνου που μοντελοποιεί κάθε νότα με ένα σήμα παραθύρου, ίδιας διάρκειας με το ημίτονο που παριστάνει κάθε νότα. Γνωρίζουμε ήδη δυο τέτοια παράθυρα: τον τετραγωνικό παλμό και τον τριγωνικό παλμό. Ουσιαστικά, ο τετραγωνικός παλμός χρησιμοποιείται έμμεσα κάθε ϕορά που δημιουργείτε ένα ημίτονο συγκεκριμένης διάρκειας. Όμως μπορείτε να χρησιμοποιήσετε άλλα παράθυρα, τέτοια ώστε να αναγκάζουν το ημιτονοειδές σήμα να κατεβαίνει ομαλά στο μηδέν στα άκρα του, όπως στο Σχήμα παρακάτω, όπου χρησιμοποιούμε το γνωστό παράθυρο Hanning, που έχει κωδωνοειδή μορφή. \n",
    "\n",
    "![win](./windowing.png)\n",
    "\n",
    "Με αυτό τον τρόπο τα clicks μειώνονται σημαντικά, ενώ δημιουργούνται και κάποια \"εξωτικά\" εφέ στον ήχο που παράγεται."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4208225a",
   "metadata": {},
   "source": [
    "Η βιβλιοθήκη `NumPy` της `Python` έχει έτοιμες συναρτήσεις παραθύρων για να χρησιμοποιήσετε. Όλες καλούνται με τον ίδιο τρόπο:\n",
    "\n",
    "`window = np.window_type(duration)`\n",
    "\n",
    "με το window_type να είναι ένα από τα εξής:\n",
    "\n",
    "`hamming, hanning, bartlett, blackman`\n",
    "\n",
    "και τη μεταβλητή `duration` να είναι η διάρκεια του παραθύρου. \n",
    "\n",
    "Εκτελέστε την παρακάτω συνάρτηση που αναλαμβάνει να πολλαπλασιάζει τα ημίτονα με το εκάστοτε παράθυρο."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a844e220",
   "metadata": {},
   "outputs": [],
   "source": [
    "def piano_simulator(lhand, rhand, ldur, rdur, sr, window):\n",
    "    \"\"\" Η συνάρτηση αυτή υλοποιεί την προηγούμενη άσκηση \n",
    "    και πολλαπλασιάζει κάθε ημίτονο με ένα γνωστό παράθυρο \n",
    "    για να μειώσει τα clicks μεταξύ γειτονικών ημιτόνων\"\n",
    "    Η μεταβλητή sr είναι η συχνότητα δειγματοληψίας \"\"\"\n",
    "\n",
    "    if window == \"hanning\":\n",
    "        win = np.hanning\n",
    "    elif window == \"hamming\":\n",
    "        win = np.hamming\n",
    "    elif window == \"bartlett\":\n",
    "        win = np.bartlett\n",
    "    elif window == \"blackman\":\n",
    "        win = np.blackman\n",
    "    else:\n",
    "        print(f\"\\\"{window}\\\" was given as the window; please provide one of the following ones: \\\"hanning\\\", \\\"hamming\\\", \\\"bartlett\\\", \\\"blackman\\\".\")\n",
    "        return 0\n",
    "\n",
    "    # Το αριστερό χέρι πρώτα\n",
    "    signal_left = []\n",
    "\n",
    "    for i in range(1, len(lhand)):\n",
    "        t = np.arange(start=0, stop=ldur[i]/1000, step=1/sr)\n",
    "        if lhand[i] == 0:\n",
    "            signal_left = np.append(signal_left, np.zeros(len(t)))\n",
    "        else:\n",
    "            freq = 440 * (2 ** ((lhand[i] - 49) / 12))\n",
    "            # Η παραθυροποίηση γίνεται εδώ:\n",
    "            signal_left = np.append(signal_left, np.sin(2*np.pi*freq*t) * win(len(t)))\n",
    "\n",
    "    # Το δεξί χέρι μετά\n",
    "    signal_right = []\n",
    "\n",
    "    for i in range(1, len(rhand)):\n",
    "        t = np.arange(start=0, stop=rdur[i]/1000, step=1/sr)\n",
    "        if rhand[i] == 0:\n",
    "            signal_right = np.append(signal_right, np.zeros(len(t)))\n",
    "        else:\n",
    "            freq = 440 * (2 ** ((rhand[i] - 49) / 12))\n",
    "            # κι εδώ:\n",
    "            signal_right = np.append(signal_right, np.sin(2*np.pi*freq*t) * win(len(t)))\n",
    "\n",
    "    # Χειριζόμαστε τη διαφορά στις διάρκειες όπως στην προηγ. άσκηση\n",
    "    d = len(signal_right) - len(signal_left)\n",
    "    signal_left = np.append(signal_left, np.zeros(d))\n",
    "\n",
    "    return signal_left + signal_right"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c8c9189f",
   "metadata": {},
   "source": [
    "Ας δούμε τι παράγει λοιπόν η παραπάνω συνάρτηση για καθένα από τα παράθυρα που εφαρμόζουμε."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "74911403",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Συχνότητα δειγματοληψίας:\n",
    "fs = 16000\n",
    "\n",
    "# Φορτώνουμε ξανά τις μεταβλητές μας\n",
    "df = pd.read_csv('FurEliseData.csv')\n",
    "lefthand = df[\"lefthand\"].values\n",
    "righthand = df[\"righthand\"].values\n",
    "lhdur = df[\"lhdur\"].values\n",
    "rhdur = df[\"rhdur\"].values\n",
    "\n",
    "# Καλούμε τη συνάρτηση για τα 4 γνωστά παράθυρα:\n",
    "hann_out = # INSERT CODE\n",
    "hamm_out = # INSERT CODE\n",
    "bart_out = # INSERT CODE\n",
    "black_out = # INSERT CODE"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4c397711",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Ας τα ακούσουμε διαδοχικά:\n",
    "Audio(hann_out, rate=fs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7d8da0b5",
   "metadata": {},
   "outputs": [],
   "source": [
    "Audio(hamm_out, rate=fs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "05751159",
   "metadata": {},
   "outputs": [],
   "source": [
    "Audio(bart_out, rate=fs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e3ed8503",
   "metadata": {},
   "outputs": [],
   "source": [
    "Audio(black_out, rate=fs)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "215f1d81",
   "metadata": {},
   "source": [
    "***\n",
    "***\n",
    "## Παραδοτέα:\n",
    "*** \n",
    "### Μετονομάστε το **συμπληρωμένο και με εκτελεσμένο κάθε κελί κώδικα** notebook σε \n",
    "\n",
    "## *HY215-Lab2-2026-CSDXXXX.ipynb*, \n",
    "\n",
    "### με *XXXX* ο αριθμός μητρώου σας. \n",
    "\n",
    "### Αν είστε με προσωρινό κωδικό λόγω μεταγραφής, τότε ονομάστε το αρχείο σας \n",
    "\n",
    "## *HY215-Lab2-2026-mtgXXXX.ipynb*, \n",
    "\n",
    "### με *XXXX* ο προσωρινός αριθμός μητρώου σας.\n",
    "\n",
    "### Αν είστε από άλλο τμήμα εκτός του CSD, τότε ονομάστε το αρχείο σας ως\n",
    "\n",
    "## *HY215-Lab2-2026-DPTXXXX.ipynb*, \n",
    "\n",
    "### με DTP ένα εκ των {MATH, PHYS} και *XXXX* ο αριθμός μητρώου σας. \n",
    "\n",
    "### Στείλτε το μετονομασμένο notebook ως επισυναπτόμενο αρχείο με e-mail στο csdp1454@csd.uoc.gr . \n",
    "\n",
    "### Στο e-mail θα αναγράφετε:\n",
    "\n",
    "### - Τίτλος: [ΗΥ215] 2ο εργαστήριο Python\n",
    "\n",
    "### και στο σώμα του e-mail τα παρακάτω: \n",
    "\n",
    "### - Ονοματεπώνυμο\n",
    "### - ΑΜ\n",
    "\n",
    "#### **Σημείωση**: μπορείτε να κάνετε επαναπαραδόσεις του εργαστηρίου - αν χρειαστεί. Χρησιμοποιήστε τίτλο\n",
    "\n",
    "#### [ΗΥ215] Επαναπαράδοση 2ου εργαστηρίου Python\n",
    "\n",
    "#### και κρατήστε τα ίδια στοιχεία στο σώμα του e-mail."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bfe46515",
   "metadata": {},
   "source": [
    "### ΠΡΟΣΕΞΤΕ: Το παραδοτέο αρχείο σας πρέπει να έχει extension .ipynb, και ΟΧΙ κάτι άλλο!!"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "my-env",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
