{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "f726c55f",
   "metadata": {},
   "source": [
    "<p>\n",
    "<font size='5' face='Georgia, Arial'>IIC-2233 Apunte Programación Avanzada</font><br>\n",
    "<font size='1'> Equipo Docente IIC2233 2026</font>\n",
    "</p>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ba14a760",
   "metadata": {},
   "source": [
    "# Tabla de contenidos\n",
    "\n",
    "1. [*QThreads* y señales](#QThreads-y-señales)\n",
    "    1. [Explicación y solución](#Explicación-y-solución)\n",
    "2. [*isAutoRepeat* en *KeyPressEvent* ](#isAutoRepeat-en-KeyPressEvent)\n",
    "3. [Sonidos en PyQT](#Sonidos-en-PyQT)\n",
    "    1. [`QMediaPlayer`](#QMediaPlayer)\n",
    "    2. [`QSoundEffect`](#QSoundEffect) \n",
    "5. [Versiones PyQT](#Versiones-PyQT)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b14e34ae",
   "metadata": {},
   "source": [
    "## *QThreads* y señales"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "06d70651",
   "metadata": {},
   "source": [
    "En los ejemplos anteriores, se muestra el uso de `Thread`, `QThread`, que envían cambios a una ventana siempre mediante **señales**. Las señales no son la única forma de conseguir este comportamiento, pero sí son las **más escalables para generar cambios** de la interfaz gráfica.\n",
    "\n",
    "A continuación se muestra un ejemplo de `QThread` que modifica directamente la posición de *labels* dentro de una ventana. Se crean 100 *threads*, cada uno con un `QLabel` que tiene un `QPixmap` de diferente color. La posición evoluciona al pasar el tiempo y en cada cambio el *thread* cambia **directamente** la posición de la etiqueta mediante `label.move()`. \n",
    "\n",
    "Si ejecutas en tu computador este código verás que funciona, pero luego de decenas o centenas de *threads* creados, el programa **colapsa** o bien no se ve nada moviéndose. Este código se encuentra en el archivo `3-pyqt-miscelaneo/1_sin_signal.py`."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9cc44c0b",
   "metadata": {},
   "source": [
    "```python\n",
    "from random import randint\n",
    "from time import sleep\n",
    "\n",
    "from PyQt6.QtCore import QThread\n",
    "from PyQt6.QtGui import QPixmap, QColor\n",
    "from PyQt6.QtWidgets import QLabel, QWidget, QApplication\n",
    "import sys\n",
    "\n",
    "\n",
    "class Cuadrado(QThread):\n",
    "    identificador = 0\n",
    "\n",
    "    def __init__(self, label: QLabel, limite_x: int, limite_y: int):\n",
    "        super().__init__()\n",
    "        self.id = Cuadrado.identificador\n",
    "        Cuadrado.identificador += 1\n",
    "\n",
    "        # guardamos el label\n",
    "        self.label = label\n",
    "\n",
    "        # Seteamos la posición inicial y la guardamos para usarla como una property\n",
    "        self._posicion = [0, 0]\n",
    "        self.posicion = [randint(0, limite_x), randint(0, limite_y)]\n",
    "\n",
    "    @property\n",
    "    def posicion(self) -> list:\n",
    "        return self._posicion\n",
    "\n",
    "    # Cada vez que se actualicé la posición,\n",
    "    # se actualiza la posición de la etiqueta\n",
    "    @posicion.setter\n",
    "    def posicion(self, nueva_posicion: list) -> None:\n",
    "        self._posicion = nueva_posicion\n",
    "        nuevo_x, nuevo_y = self.posicion\n",
    "        self.label.move(nuevo_x, nuevo_y)\n",
    "\n",
    "    def run(self) -> None:\n",
    "        while True:\n",
    "            sleep(0.1)\n",
    "            nuevo_x = self.posicion[0] + randint(-2, 2)\n",
    "            nuevo_y = self.posicion[1] + randint(-2, 2)\n",
    "            self.posicion = [nuevo_x, nuevo_y]\n",
    "\n",
    "\n",
    "class MiVentana(QWidget):\n",
    "    def __init__(self):\n",
    "        super().__init__()\n",
    "        self.setWindowTitle(\"No uso de señales\")\n",
    "        self.setGeometry(200, 200, 500, 500)\n",
    "\n",
    "        # Definimos QLabel para el fondo de la ventana\n",
    "        self.fondo = QLabel(self)\n",
    "        self.fondo.setStyleSheet(\"background: orange\")\n",
    "        self.fondo.setGeometry(0, 0, 500, 500)\n",
    "\n",
    "        self.cuadrados = []\n",
    "        self.labels = {}\n",
    "\n",
    "        for _ in range(100):\n",
    "            self.crear_cuadrado()\n",
    "\n",
    "        self.show()\n",
    "\n",
    "    def crear_cuadrado(self) -> None:\n",
    "        # Creamos el label y se lo pasamos al Cuadrado\n",
    "        label = QLabel(self)\n",
    "        label.setGeometry(-50, -50, 50, 50)\n",
    "\n",
    "        # Creamos un QPixmap de color aleatorio\n",
    "        pixmap = QPixmap(50, 50)\n",
    "        pixmap.fill(QColor(randint(20, 200), randint(20, 200), randint(20, 200)))\n",
    "        label.setPixmap(pixmap)\n",
    "        label.show()\n",
    "\n",
    "        nuevo_cuadrado = Cuadrado(label, self.width(), self.height())\n",
    "        self.cuadrados.append(nuevo_cuadrado)\n",
    "        nuevo_cuadrado.start()\n",
    "\n",
    "\n",
    "if __name__ == \"__main__\":\n",
    "\n",
    "    def hook(type_, value, traceback):\n",
    "        print(type_)\n",
    "        print(traceback)\n",
    "\n",
    "    sys.__excepthook__ = hook\n",
    "    app = QApplication([])\n",
    "    ventana = MiVentana()\n",
    "    sys.exit(app.exec())\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7fdced88",
   "metadata": {},
   "source": [
    "### Explicación y solución\n",
    "\n",
    "Esto ocurre debido a que hay múltiples *threads* haciendo cambios directos en la interfaz casi simultáneamente, lo que da espacio para potenciales errores de concurrencia, que son clásicos en *threads*.\n",
    "\n",
    "En cambio, al usar señales se evade de mejor forma este problema ya que se delega al manejo de eventos de PyQt la realización de los cambios en la interfaz. El siguiente código muestra una adaptación del código anterior pero utilizando señales. Al ejecutarlo, verás que se actualizan de mejor forma los Labels en la ventana y no se cae con los 100 *threads* iniciados. Este código se encuentra en el archivo `3-pyqt-miscelaneo/2_con_signal.py`.."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cd21fc5b",
   "metadata": {},
   "source": [
    "```python\n",
    "from random import randint\n",
    "from time import sleep\n",
    "\n",
    "from PyQt6.QtCore import QThread, pyqtSignal\n",
    "from PyQt6.QtGui import QPixmap, QColor\n",
    "from PyQt6.QtWidgets import QLabel, QWidget, QApplication\n",
    "import sys\n",
    "\n",
    "\n",
    "class Cuadrado(QThread):\n",
    "    identificador = 0\n",
    "\n",
    "    def __init__(self, senal_mover: pyqtSignal, limite_x: int, limite_y: int) -> None:\n",
    "        super().__init__()\n",
    "        self.id = Cuadrado.identificador\n",
    "        Cuadrado.identificador += 1\n",
    "\n",
    "        # guardamos la señal\n",
    "        self.senal_mover = senal_mover\n",
    "\n",
    "        # Seteamos la posición inicial y la guardamos para usarla como una property\n",
    "        self._posicion = [0, 0]\n",
    "        self.posicion = (randint(0, limite_x), randint(0, limite_y))\n",
    "\n",
    "    @property\n",
    "    def posicion(self) -> list:\n",
    "        return self._posicion\n",
    "\n",
    "    # Cada vez que se actualice la posición,\n",
    "    # se actualiza la posición de la etiqueta\n",
    "    @posicion.setter\n",
    "    def posicion(self, nueva_posicion: list) -> None:\n",
    "        self._posicion = nueva_posicion\n",
    "        nuevo_x, nuevo_y = self.posicion\n",
    "        self.senal_mover.emit(self.id, nuevo_x, nuevo_y)\n",
    "\n",
    "    def run(self):\n",
    "        while True:\n",
    "            sleep(0.1)\n",
    "            nuevo_x = self.posicion[0] + randint(-2, 2)\n",
    "            nuevo_y = self.posicion[1] + randint(-2, 2)\n",
    "            self.posicion = (nuevo_x, nuevo_y)\n",
    "\n",
    "\n",
    "class MiVentana(QWidget):\n",
    "    senal_mover = pyqtSignal(int, int, int)\n",
    "\n",
    "    def __init__(self) -> None:\n",
    "        super().__init__()\n",
    "        self.setWindowTitle(\"Correcto uso de señales\")\n",
    "        self.setGeometry(200, 200, 500, 500)\n",
    "\n",
    "        # Definimos QLabel para el fondo de la ventana\n",
    "        self.fondo = QLabel(self)\n",
    "        self.fondo.setStyleSheet(\"background: orange\")\n",
    "        self.fondo.setGeometry(0, 0, 500, 500)\n",
    "\n",
    "        self.cuadrados = []\n",
    "        self.labels = {}\n",
    "\n",
    "        for _ in range(100):\n",
    "            self.crear_cuadrado()\n",
    "\n",
    "        self.senal_mover.connect(self.mover)\n",
    "        self.show()\n",
    "\n",
    "    def crear_cuadrado(self) -> None:\n",
    "        # Creamos el label y se lo pasamos al Cuadrado\n",
    "        label = QLabel(self)\n",
    "        label.setGeometry(-50, -50, 50, 50)\n",
    "\n",
    "        # Creamos un QPixmap de color aleatorio\n",
    "        pixmap = QPixmap(50, 50)\n",
    "        pixmap.fill(QColor(randint(20, 200), randint(20, 200), randint(20, 200)))\n",
    "        label.setPixmap(pixmap)\n",
    "        label.show()\n",
    "\n",
    "        nuevo_cuadrado = Cuadrado(self.senal_mover, self.width(), self.height())\n",
    "        self.labels[nuevo_cuadrado.id] = label\n",
    "        self.cuadrados.append(nuevo_cuadrado)\n",
    "        nuevo_cuadrado.start()\n",
    "\n",
    "    def mover(self, id: int, x: int, y: int) -> None:\n",
    "        self.labels[id].move(x, y)\n",
    "\n",
    "\n",
    "if __name__ == \"__main__\":\n",
    "\n",
    "    def hook(type_, value, traceback):\n",
    "        print(type_)\n",
    "        print(traceback)\n",
    "\n",
    "    sys.__excepthook__ = hook\n",
    "    app = QApplication([])\n",
    "    ventana = MiVentana()\n",
    "    sys.exit(app.exec())\n",
    "\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1784d71a",
   "metadata": {},
   "source": [
    "La conclusión de este experimento es que, en general, el uso de señales en conjunto a *theading* permite evadir potenciales problemas de concurrencia."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2c6d14bf",
   "metadata": {},
   "source": [
    "## *isAutoRepeat* en *KeyPressEvent* "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fd3fe5cc",
   "metadata": {},
   "source": [
    "Anteriormente aprendimos del método `keyPressEvent` para detectar cuando una tecla del teclado es presionada. Una situación de interés de este método es cuando mantenemos presionada una tecla por mucho tiempo. Por defecto, cuando ocurre este suceso, el método es llamado múltiples veces, pero hay situaciones donde no queremos que se ejecute múltiples veces el método si es que mantengo presionado. \n",
    "\n",
    "Aquí es donde sale al rescate el método `isAutoRepeat()`. Este método retorna un booleano que indica si el evento gatillado fue producto de presionar por primera vez una tecla (`evento.isAutoRepeat() == False`) o es la repetición del evento por mantener presionada la tecla (`evento.isAutoRepeat() == True`).\n",
    "\n",
    "A continuación veremos un ejemplo donde detectamos y contamos cuántas veces entramos en un `if` cuando presionamos la letra `A` o la `W`. La diferencia es que la letra `A` incluye una verificación de que `evento.isAutoRepeat()` sea `False`. Este código se encuentra en el archivo `3-pyqt-miscelaneo/3_autorepeat.py`.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5207469c",
   "metadata": {},
   "source": [
    "```python\n",
    "import sys\n",
    "from PyQt6.QtCore import Qt\n",
    "from PyQt6.QtGui import QKeyEvent\n",
    "from PyQt6.QtWidgets import QApplication, QWidget, QLabel\n",
    "\n",
    "\n",
    "class MiVentana(QWidget):\n",
    "    def __init__(self) -> None:\n",
    "        super().__init__()\n",
    "        # Configuramos los widgets de la interfaz\n",
    "        self.label_w = QLabel(\"Presiona la tecla W\", self)\n",
    "        self.label_w_contador = QLabel(\"Presionada 0 veces\", self)\n",
    "        self.contador_w = 0\n",
    "\n",
    "        self.label_a = QLabel(\"Presiona la tecla A\", self)\n",
    "        self.label_a_contador = QLabel(\"Presionada 0 veces\", self)\n",
    "        self.contador_a = 0\n",
    "\n",
    "        self.label_w.setGeometry(10, 10, 230, 30)\n",
    "        self.label_w_contador.setGeometry(10, 40, 230, 30)\n",
    "        self.label_a.setGeometry(10, 100, 230, 30)\n",
    "        self.label_a_contador.setGeometry(10, 130, 230, 30)\n",
    "\n",
    "        # Configuramos las propiedades de la ventana.\n",
    "        self.setWindowTitle(\"Ejemplo isAutoRepeat\")\n",
    "        self.setGeometry(50, 50, 250, 200)\n",
    "        self.show()\n",
    "\n",
    "    def keyPressEvent(self, evento: QKeyEvent) -> None:\n",
    "        if evento.key() == Qt.Key.Key_W:\n",
    "            self.contador_w += 1\n",
    "            self.label_w_contador.setText(f\"Presionada {self.contador_w} veces\")\n",
    "\n",
    "        if evento.key() == Qt.Key.Key_A and not evento.isAutoRepeat():\n",
    "            self.contador_a += 1\n",
    "            self.label_a_contador.setText(f\"Presionada {self.contador_a} veces\")\n",
    "\n",
    "\n",
    "if __name__ == \"__main__\":\n",
    "\n",
    "    def hook(type_, value, traceback):\n",
    "        print(type_)\n",
    "        print(traceback)\n",
    "\n",
    "    sys.__excepthook__ = hook\n",
    "    app = QApplication([])\n",
    "    ventana = MiVentana()\n",
    "    sys.exit(app.exec())\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b0cd927b",
   "metadata": {},
   "source": [
    "## Sonidos en PyQT"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bcac2a13",
   "metadata": {},
   "source": [
    "Cuando uno interactúa con una interfaz gráfica, no puede faltar su música de fondo o algún sonido cuando ocurre algún evento. Por esto, ahora veremos cómo podemos reproducir archivos de audio desde la interfaz. En esta ocasión, veremos 2 objetos de PyQT para reproducir sonido.\n",
    "\n",
    "Para ambos casos, vamos a utilizar un objeto de PyQt que nos construirá un _path_ absoluto a partir de un _path_ relativo. Esta acción es necesaria para que PyQt sea capaz de reproducir los sonidos.\n",
    "\n",
    "**Warning**. Para los que tienen **Windows**, es posible llegar a tener un problema si el _path_ absoluto tiene un largo mayor a 260 caracteres. Esto es por una limitación del propio Sistema Operativo que solo se puede deshabilitar con permisos de administrador. Para evitar este error, se recomienda no tener los códigos de IIC2233 en un directorio profundo del computador, así el _path_ absoluto no será mayor a 260. O bien, reinstalar Python y asegurar de presionar la opción _\"Disable path length limit\"_.\n",
    "\n",
    "### `QMediaPlayer`\n",
    "\n",
    "Este objeto nos permite reproducir archivos `.mp3`. Para configurar correctamente este objeto, se debe:\n",
    "\n",
    "1. Instanciar el objeto: `self.player = QMediaPlayer(self)`\n",
    "2. Instanciar un objeto: `self.audio_output = QAudioOutput(self)`\n",
    "3. Agregar el audio output al player: `self.player.setAudioOutput(self.audio_output)`\n",
    "4. Obtener, **dinámicamente**, el _path_ absoluto al sonido que queremos reproducir a partir de un _path_ relativo: `path = os.path.abspath(join(\"sounds\", \"waku-waku.mp3\"))`.\n",
    "5. Definir un objeto tipo `QURL` con el _path_ a nuestro sonido: `file_url = QUrl.fromLocalFile(path)`.\n",
    "6. Entregar el objeto tipo `QURL` al reproductor: `self.player.setSource(file_url)`\n",
    "7. Usar `.play()` para reproducir el sonido.\n",
    "    \n",
    "**Importante**: por definición, el formato MP3 utiliza un algoritmo con pérdida para conseguir un menor tamaño de archivo. Esto puede ocasionar que al momento de reproducir un sonido con `QMediaPlayer`, salga el siguiente _warning_ en la consola: _Could not update timestamps for skipped samples_ o _Could not update timestamps for discarded samples._ Para efectos de este curso, no te preocupes por dicho _warning_.\n",
    "    \n",
    "    \n",
    "### `QSoundEffect`\n",
    "\n",
    "Este objeto nos permite reproducir archivos `.wav`. Para configurar correctamente este objeto, se debe: \n",
    "\n",
    "1. Instanciar el objeto: `self.media_player_wav = QSoundEffect(self)`\n",
    "2. Definir un objeto tipo `QURL` con el _path_ a nuestro sonido: `file_url = QUrl.fromLocalFile(join(\"sounds\", \"see-you-again.wav\"))`.\n",
    "3. Entregarle la URL a nuestro reproductor: `self.media_player_wav.setSource(file_url)`\n",
    "4. Usar `.play()` para reproducir el sonido. También podemos usar `stop()` para detenerlo."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bb97bcb7",
   "metadata": {},
   "source": [
    "A continuación se muestra un ejemplo donde utilizamos ambos objetos para reproducir 2 sonidos distintos. Este código se encuentra en el archivo `3-pyqt-miscelaneo/4_sonidos.py`. Dado el uso de _paths_ relativos, la terminal **debe** estar posicionada dentro del directorio `scripts/3-pyqt-miscelaneo` para hacer `python3 4_sonidos.py`."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0eaf6447",
   "metadata": {},
   "source": [
    "```python\n",
    "import sys\n",
    "import os\n",
    "from PyQt6.QtCore import QUrl\n",
    "from PyQt6.QtWidgets import QApplication, QWidget, QPushButton\n",
    "from PyQt6.QtMultimedia import QMediaPlayer, QSoundEffect, QAudioOutput\n",
    "from os.path import join\n",
    "\n",
    "\n",
    "class MiVentana(QWidget):\n",
    "    def __init__(self) -> None:\n",
    "        super().__init__()\n",
    "\n",
    "        # Configuramos los widgets de la interfaz\n",
    "        self.boton_sorpresa = QPushButton(\"Wooow\", self)\n",
    "        self.boton_empezar = QPushButton(\"Empezar música de fondo\", self)\n",
    "        self.boton_parar = QPushButton(\"Parar música de fondo\", self)\n",
    "\n",
    "        self.boton_sorpresa.setGeometry(10, 10, 230, 30)\n",
    "        self.boton_empezar.setGeometry(10, 50, 230, 30)\n",
    "        self.boton_parar.setGeometry(10, 90, 230, 30)\n",
    "\n",
    "        self.boton_sorpresa.clicked.connect(self.empezar_sonido_sorpresa)\n",
    "        self.boton_empezar.clicked.connect(self.empezar_musica_fondo)\n",
    "        self.boton_parar.clicked.connect(self.parar_musica_fondo)\n",
    "\n",
    "        # Configuramos las propiedades de la ventana.\n",
    "        self.setWindowTitle(\"Ejemplo Sonidos\")\n",
    "        self.setGeometry(50, 50, 250, 200)\n",
    "\n",
    "        # Opción MP3: QMediaPlayer en PyQt6 tiene pequeñas diferencias\n",
    "        # 1. Para que QMediaPlayer reproduzca audio, se debe agregar un QAudioOutput\n",
    "        # 2. Necesitas utilizar el path absoluto. Para lograr generarlo correctamente\n",
    "        #    usaremos os.path.abspath que transforma el relativo a absoluto según cada PC\n",
    "        # 3. Se utiliza setSource para indicar el path al audio a escuchar\n",
    "        self.player = QMediaPlayer(self)\n",
    "        self.audio_output = QAudioOutput(self)\n",
    "        self.player.setAudioOutput(self.audio_output)\n",
    "        path = os.path.abspath(join(\"sounds\", \"waku-waku.mp3\"))\n",
    "        self.player.setSource(QUrl.fromLocalFile(path))\n",
    "\n",
    "        # Opción Wav: QSoundEffect\n",
    "        self.media_player_wav = QSoundEffect(self)\n",
    "        self.media_player_wav.setVolume(0.1)  # Opcional\n",
    "        file_url = QUrl.fromLocalFile(join(\"sounds\", \"see-you-again.wav\"))\n",
    "        self.media_player_wav.setSource(file_url)\n",
    "\n",
    "        # Mostrar ventana\n",
    "        self.show()\n",
    "\n",
    "    def empezar_sonido_sorpresa(self) -> None:\n",
    "        self.player.play()\n",
    "\n",
    "    def empezar_musica_fondo(self) -> None:\n",
    "        if not self.media_player_wav.isPlaying():\n",
    "            self.media_player_wav.play()\n",
    "\n",
    "    def parar_musica_fondo(self) -> None:\n",
    "        if self.media_player_wav.isPlaying():\n",
    "            self.media_player_wav.stop()\n",
    "\n",
    "\n",
    "if __name__ == \"__main__\":\n",
    "\n",
    "    def hook(type_, value, traceback):\n",
    "        print(type_)\n",
    "        print(traceback)\n",
    "\n",
    "    sys.__excepthook__ = hook\n",
    "    app = QApplication([])\n",
    "    ventana = MiVentana()\n",
    "    sys.exit(app.exec())\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f69a154a-f5c8-4846-8fe7-bf26310536ad",
   "metadata": {},
   "source": [
    "## Versiones PyQT\n",
    "\n",
    "Ahora que vimos todo lo relevante de `PyQt6`, es importante mencionar que todo lo enseñado en estos contenidos es pensando en el uso de `PyQt6`, si por eventualidades decides utilizar otra versión de `PyQt`, por ejemplo `PyQt5`, puede que sea necesario ajustar los códigos para que funcionen en dicha versión. No siempre los códigos de una versión son iguales a otra, a veces un código sí funcionará en diferentes versiones, pero otras veces no.\n",
    "\n",
    "\n",
    "Para ejemplificar esta situación, podemos analizar cómo reproducir un sonido MP3 con PyQt6 y PyQt5:\n",
    "\n",
    "* Versión `PyQt5`\n",
    "    ```python\n",
    "    self.player = QMediaPlayer(self)\n",
    "    path = os.path.abspath(join(\"sounds\", \"waku-waku.mp3\"))\n",
    "    content = QMediaContent(QUrl.fromLocalFile(path))\n",
    "    self.player.setMedia(content)\n",
    "    ```\n",
    "<br>\n",
    "\n",
    "* Versión `PyQt6`\n",
    "    ```python\n",
    "    self.media_player_mp3 = QMediaPlayer(self)\n",
    "    self.media_player_mp3.setAudioOutput(QAudioOutput(self))\n",
    "    file_url = QUrl.fromLocalFile(join(\"sounds\", \"waku-waku.mp3\"))\n",
    "    self.media_player_mp3.setSource(file_url)\n",
    "    \n",
    "    ```\n",
    "\n",
    "Se puede apreciar que en `PyQt6` usando un método llamado `setAudioOutput` y un objeto llamado `QAudioOutput` en vez de `QMediaContent` que es ocupado en `PyQt5`. Esto ocurre porque justamente de una versión a otra, los creadores de una librería pueden alterar las clases del código, y esto es aplicable a cualquier librería y en cualquier lenguaje de programación. Por lo tanto, si decides ocupar otra versión de una librería en el futuro, mucha precaución de que será necesario iterar los códigos para verificar si funcionan."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "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.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
