{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "83a25358",
   "metadata": {},
   "source": [
    "# <span style=\"color:#306998\">Python</span><span style=\"color:#FFD43B\">Here</span> 0.2.2 release intro\n",
    "\n",
    "The intro was generated with `%%there ai` to show the PythonHere 0.2.x release features as a short audiovisual app.\n",
    "\n",
    "Watch the result as a YouTube Short: https://www.youtube.com/shorts/ExN0YnTljVc\n",
    "\n",
    "## Connect"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d7a23e47",
   "metadata": {},
   "outputs": [],
   "source": [
    "%load_ext pythonhere\n",
    "%connect-there"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d30157c6",
   "metadata": {},
   "source": [
    "## Prompt used to generate the intro"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d34799c3",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%there ai --prompts midi\n",
    "You are the PythonHere 0.2.x release.\n",
    "Express yourself as a short audiovisual demoscene-style intro.\n",
    "\n",
    "Goal: generate a compact audiovisual intro.\n",
    "\n",
    "Your release features:\n",
    "\n",
    "```python\n",
    "announcements = [\n",
    "    {\n",
    "        \"title\": \"PythonHere 0.2.*\",\n",
    "        \"subtitle\": \"the stack wakes again\"\n",
    "    },\n",
    "    {\n",
    "        \"title\": \"GitHub Releases\",\n",
    "        \"subtitle\": \"APK builds are back\"\n",
    "    },\n",
    "    {\n",
    "        \"title\": \"%%there get {variable}\",\n",
    "        \"subtitle\": \"get remote variables\"\n",
    "    },\n",
    "    {\n",
    "        \"title\": \"%%there download {file}\",\n",
    "        \"subtitle\": \"pull files from the target\"\n",
    "    },\n",
    "    {\n",
    "        \"title\": \"%%there ai\",\n",
    "        \"subtitle\": \"prompt -> cell\"\n",
    "    },\n",
    "    {\n",
    "        \"title\": \"Generate -> Edit -> Run\",\n",
    "        \"subtitle\": \"generated cells stay under your control\"\n",
    "    },\n",
    "    {\n",
    "        \"title\": \"%%there ai --prompts masterpiece\",\n",
    "        \"subtitle\": \"custom context / custom style\"\n",
    "    },\n",
    "    {\n",
    "        \"title\": \"%%there ai --fix\",\n",
    "        \"subtitle\": \"failure becomes feedback\"\n",
    "    },\n",
    "]\n",
    "```\n",
    "\n",
    "Creative direction:\n",
    "Make it feel like a compact demoscene intro built out of Kivy itself.\n",
    "\n",
    "Use:\n",
    "\n",
    "* dark background\n",
    "* neon / terminal / retro-futurist mood\n",
    "* rhythmic motion\n",
    "* Kivy widgets used playfully as visual objects\n",
    "* text effects\n",
    "* short readable text beats\n",
    "* autoplay when ready\n",
    "* endless loop after the intro sequence\n",
    "* single feature display at a time\n",
    "\n",
    "Title is: PythonHere 0.2.x.\n",
    "\n",
    "Make the version beat-synced:\n",
    "\n",
    "* show `0.2.0`\n",
    "* then `0.2.1`\n",
    "* then `0.2.2`\n",
    "* then `0.2.x`\n",
    "* repeat this pattern every 16 sequencer steps\n",
    "\n",
    "Make `Python` and `Here` with separate colors:\n",
    "\n",
    "* Python: `#306998`\n",
    "* Here: `#FFD43B`\n",
    "* version: smaller cyan/white text\n",
    "\n",
    "Core timing model:\n",
    "Use one explicit 8-second / 64-step timing grid.\n",
    "\n",
    "Constants must be:\n",
    "\n",
    "```python\n",
    "INTRO_SECONDS = 8.0\n",
    "STEPS_PER_LOOP = 64\n",
    "TICK_SECONDS = INTRO_SECONDS / STEPS_PER_LOOP\n",
    "VISUAL_FPS = 30.0\n",
    "\n",
    "FEATURES_PER_LOOP = 8\n",
    "FEATURE_STEPS = STEPS_PER_LOOP // FEATURES_PER_LOOP  # 8\n",
    "\n",
    "BLOCKS_PER_LOOP = 4\n",
    "BLOCK_STEPS = STEPS_PER_LOOP // BLOCKS_PER_LOOP      # 16\n",
    "```\n",
    "\n",
    "Everything must derive from this sequencer grid:\n",
    "\n",
    "* loop step: `0..63`\n",
    "* musical block: `step // BLOCK_STEPS`\n",
    "* local block step: `step % BLOCK_STEPS`\n",
    "* feature card: `step // FEATURE_STEPS`\n",
    "* beat marker: `step % BLOCK_STEPS`\n",
    "* version character: `(step % BLOCK_STEPS) // 4`\n",
    "\n",
    "The intro must be perfectly looped in 8 seconds:\n",
    "\n",
    "* 64 sequencer steps total\n",
    "* 8 feature cards total\n",
    "* each feature card lasts exactly 8 sequencer steps\n",
    "* 4 musical blocks total\n",
    "* each musical block lasts exactly 16 sequencer steps\n",
    "* loop returns exactly to the initial visual and musical state at step 0\n",
    "\n",
    "Sequencer / MIDI clock:\n",
    "The intro should be built around a live MIDI sequencer.\n",
    "\n",
    "The sequencer step is the master state.\n",
    "The Kivy Clock interval that calls `midi_tick` is the sequencer clock.\n",
    "`midi_tick` advances exactly one integer step each tick.\n",
    "Visuals must follow the sequencer state.\n",
    "\n",
    "Do not derive visual state directly from `loop_start_time`.\n",
    "Do not use `phase * len(announcements)` for feature cards.\n",
    "Do not use independent wall-clock animation as the master.\n",
    "\n",
    "Keep this state:\n",
    "\n",
    "```python\n",
    "self.step\n",
    "self.last_played_step\n",
    "self.last_tick_time\n",
    "```\n",
    "\n",
    "Use this helper for visual timing:\n",
    "\n",
    "```python\n",
    "def _loop_position(self):\n",
    "    elapsed_since_tick = max(0.0, time.perf_counter() - self.last_tick_time)\n",
    "    micro = min(0.999, elapsed_since_tick / TICK_SECONDS)\n",
    "\n",
    "    step_float = self.last_played_step + micro\n",
    "    step_float %= STEPS_PER_LOOP\n",
    "\n",
    "    step = int(step_float)\n",
    "    phase = step_float / STEPS_PER_LOOP\n",
    "\n",
    "    return step, micro, phase\n",
    "```\n",
    "\n",
    "In `midi_tick`:\n",
    "\n",
    "* if stopped, return `False`\n",
    "* at the start of each sequencer tick, decrement active note lifetimes\n",
    "* send `note_off` for notes whose `remaining_steps` reaches zero\n",
    "* then generate MIDI events for the current integer `step`\n",
    "* then send the new `note_on` events\n",
    "* then add those notes to the active note list with `remaining_steps`\n",
    "* then set `last_played_step = played_step`\n",
    "* then set `last_tick_time = time.perf_counter()`\n",
    "* then advance `step = (step + 1) % STEPS_PER_LOOP`\n",
    "\n",
    "In `visual_tick`:\n",
    "\n",
    "* never schedule MIDI\n",
    "* never play MIDI\n",
    "* never stop MIDI\n",
    "* only read sequencer state\n",
    "* call `_loop_position()`\n",
    "* derive all visual state from `visual_step`, `micro`, and `phase`\n",
    "\n",
    "Use this visual timing pattern:\n",
    "\n",
    "```python\n",
    "visual_step, micro, phase = self._loop_position()\n",
    "\n",
    "feature_index = min(\n",
    "    len(announcements) - 1,\n",
    "    visual_step // FEATURE_STEPS,\n",
    ")\n",
    "\n",
    "local_feature_phase = (\n",
    "    (visual_step % FEATURE_STEPS) + micro\n",
    ") / FEATURE_STEPS\n",
    "\n",
    "active16 = visual_step % BLOCK_STEPS\n",
    "\n",
    "version_char = [\"0\", \"1\", \"2\", \"x\"][\n",
    "    (visual_step % BLOCK_STEPS) // 4\n",
    "]\n",
    "```\n",
    "\n",
    "Requirements:\n",
    "\n",
    "* The intro should be built around a live MIDI sequencer.\n",
    "* The sequencer state is the master clock.\n",
    "* Visuals follow the sequencer state.\n",
    "* Portrait Android mode.\n",
    "* Text should always fit screen and containers.\n",
    "* The result should not look like a normal app screen.\n",
    "* Intro should be perfectly looped in 8 seconds.\n",
    "* Should show all 8 features in 8 seconds.\n",
    "* Should loop to exactly the initial state.\n",
    "* Avoid audio slowdown by keeping `midi_tick` very lightweight.\n",
    "\n",
    "Audio:\n",
    "Generate and play a live MIDI looping synthpop positive rhythmic tune.\n",
    "\n",
    "The tune must follow the 64-step grid:\n",
    "\n",
    "* 64 steps per loop\n",
    "* 4 musical blocks\n",
    "* 16 steps per musical block\n",
    "* use `block = step // BLOCK_STEPS`\n",
    "* use `local = step % BLOCK_STEPS`\n",
    "* drums, bass, lead, and chords should all loop exactly at step 64\n",
    "* keep the MIDI event count per tick small\n",
    "* avoid heavy computation inside `midi_tick`\n",
    "\n",
    "Note lifetime:\n",
    "Do not use `Clock.schedule_once` for every `note_off`.\n",
    "\n",
    "Instead, keep a list of active notes with `remaining_steps`.\n",
    "\n",
    "At the start of each sequencer tick:\n",
    "\n",
    "* decrement `remaining_steps`\n",
    "* send `note_off` for notes whose `remaining_steps` reaches zero\n",
    "\n",
    "Then send new `note_on` events and add them to the active note list.\n",
    "\n",
    "This keeps MIDI event scheduling deterministic and avoids many tiny callbacks.\n",
    "\n",
    "Kivy visual concept:\n",
    "Use ordinary Kivy widgets in extraordinary ways.\n",
    "\n",
    "For example:\n",
    "\n",
    "* Labels as glowing title cards\n",
    "* Buttons as pulsing blocks or beat markers\n",
    "* Sliders as oscilloscopes\n",
    "* ProgressBars as equalizers\n",
    "* BoxLayouts as moving panels\n",
    "* canvas lines, grids, particles, waves, scanlines, or glow-like effects\n",
    "\n",
    "The result should not look like a normal app screen.\n",
    "It should look like a tiny audiovisual intro made from the runtime’s own UI system.\n",
    "\n",
    "Kivy scheduling:\n",
    "Use Kivy Clock carefully.\n",
    "\n",
    "Allowed:\n",
    "\n",
    "* one `Clock.schedule_interval` for the MIDI sequencer tick\n",
    "* one `Clock.schedule_interval` for lightweight visual refresh\n",
    "\n",
    "Not allowed:\n",
    "\n",
    "* many scheduled callbacks\n",
    "* `Clock.schedule_once` for every MIDI note-off\n",
    "* rebuilding layouts every frame\n",
    "* creating/destroying widgets during animation\n",
    "* recalculating installed distributions during animation\n",
    "* calling `texture_update` every frame\n",
    "* scheduling MIDI events from the visual update function\n",
    "* creating new canvas instructions during animation\n",
    "* creating large temporary lists during animation\n",
    "\n",
    "Performance:\n",
    "\n",
    "* `midi_tick` must be extremely lightweight\n",
    "* package scanning must happen once before animation starts\n",
    "* runtime ticker string must be built once before animation starts\n",
    "* widgets must be created once\n",
    "* canvas instructions must be created once\n",
    "* animation should update existing widget properties and existing canvas instructions only\n",
    "* visual update may set existing `Color.rgba`, `Line.points`, `Rectangle.pos`, `Rectangle.size`, `Label.text`, `Label.opacity`, `Slider.value`, and `ProgressBar.value`\n",
    "* avoid expensive work in both scheduled callbacks\n",
    "* never let visual effects slow down MIDI timing\n",
    "\n",
    "Effects:\n",
    "Use canvas for effects.\n",
    "\n",
    "Runtime scroller:\n",
    "Create a continuous demoscene-style ticker at the bottom or edge of the screen.\n",
    "\n",
    "The scroller must use real runtime information, not hardcoded fake package versions.\n",
    "\n",
    "Build it dynamically once before animation starts:\n",
    "\n",
    "* Use `sys.version_info` for the real Python interpreter version\n",
    "* Use `[f\"{dist.metadata['Name']}=={dist.version}\" for dist in distributions()]` for installed packages\n",
    "* Format the ticker like: `PYTHON==3.x.x | Kivy==2.3.1 | cffi==... | ...`\n",
    "\n",
    "Ticker timing:\n",
    "\n",
    "* ticker starts outside the right edge of the screen\n",
    "* text is not visible at the first frame\n",
    "* ticker moves from right to left\n",
    "* ticker position is derived from sequencer `phase`\n",
    "* ticker returns to exactly the initial outside-right position when `phase` wraps to `0`\n",
    "* perfect loop sync is more important than showing every package clearly\n",
    "* if the ticker text is very long, it does not need to fully reveal every package in one loop\n",
    "\n",
    "Use this ticker motion model:\n",
    "\n",
    "```python\n",
    "ticker.x = self.ui.width - phase * (self.ui.width + self.ui._ticker_width)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "82db05a0",
   "metadata": {},
   "source": [
    "## Generated intro code"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "944f8baa",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%there\n",
    "# Generated locally by %%there ai. Review before running.\n",
    "from importlib.metadata import distributions\n",
    "import math\n",
    "import sys\n",
    "import time\n",
    "\n",
    "from kivy.clock import Clock\n",
    "from kivy.graphics import Color, Ellipse, Line, Rectangle\n",
    "from kivy.lang import Builder\n",
    "from kivy.logger import Logger\n",
    "from kivy.metrics import dp, sp\n",
    "from kivy.uix.button import Button\n",
    "from kivy.uix.floatlayout import FloatLayout\n",
    "from kivy.uix.popup import Popup\n",
    "from kivy.uix.progressbar import ProgressBar\n",
    "from kivy.uix.label import Label\n",
    "\n",
    "from midistream import Synthesizer, MIDIException, ReverbPreset\n",
    "from midistream.helpers import (\n",
    "    Control,\n",
    "    midi_control_change,\n",
    "    midi_instruments,\n",
    "    midi_note_off,\n",
    "    midi_note_on,\n",
    "    midi_program_change,\n",
    ")\n",
    "\n",
    "INTRO_SECONDS = 8.0\n",
    "STEPS_PER_LOOP = 64\n",
    "TICK_SECONDS = INTRO_SECONDS / STEPS_PER_LOOP\n",
    "VISUAL_FPS = 30.0\n",
    "\n",
    "FEATURES_PER_LOOP = 8\n",
    "FEATURE_STEPS = STEPS_PER_LOOP // FEATURES_PER_LOOP  # 8\n",
    "\n",
    "BLOCKS_PER_LOOP = 4\n",
    "BLOCK_STEPS = STEPS_PER_LOOP // BLOCKS_PER_LOOP      # 16\n",
    "\n",
    "announcements = [\n",
    "    {\"title\": \"PythonHere 0.2.*\", \"subtitle\": \"the stack wakes again\"},\n",
    "    {\"title\": \"GitHub Releases\", \"subtitle\": \"APK builds are back\"},\n",
    "    {\"title\": \"%%there get {variable}\", \"subtitle\": \"get remote variables\"},\n",
    "    {\"title\": \"%%there download {path}\", \"subtitle\": \"pull files from the target\"},\n",
    "    {\"title\": \"%%there ai\", \"subtitle\": \"prompt -> cell\"},\n",
    "    {\"title\": \"Generate -> Edit -> Run\", \"subtitle\": \"generated cells stay under your control\"},\n",
    "    {\"title\": \"%%there ai --prompts masterpiece\", \"subtitle\": \"custom context / custom style\"},\n",
    "    {\"title\": \"%%there ai --fix\", \"subtitle\": \"failure becomes feedback\"},\n",
    "]\n",
    "\n",
    "try:\n",
    "    previous_intro = globals().get(\"pythonhere_intro_controller\")\n",
    "    if previous_intro is not None:\n",
    "        previous_intro.stop(close_midi=False)\n",
    "except Exception:\n",
    "    Logger.exception(\"PythonHere: Could not stop previous intro\")\n",
    "\n",
    "if \"midistream_active_notes\" not in globals():\n",
    "    midistream_active_notes = set()\n",
    "if \"midistream_used_channels\" not in globals():\n",
    "    midistream_used_channels = set()\n",
    "\n",
    "installed_runtime_packages = [f\"{dist.metadata['Name']}=={dist.version}\" for dist in distributions()]\n",
    "pythonhere_intro_ticker_text = (\n",
    "    f\"PYTHON=={sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}\"\n",
    "    + \" | \"\n",
    "    + \" | \".join(installed_runtime_packages)\n",
    ")\n",
    "\n",
    "pythonhere_intro_state = {\n",
    "    \"ok\": False,\n",
    "    \"stage\": \"initializing\",\n",
    "    \"message\": \"Building PythonHere 0.2.x intro\",\n",
    "    \"error\": None,\n",
    "}\n",
    "\n",
    "\n",
    "def get_synthesizer():\n",
    "    global synthesizer\n",
    "    if \"synthesizer\" not in globals() or synthesizer is None:\n",
    "        synthesizer = Synthesizer()\n",
    "    return synthesizer\n",
    "\n",
    "\n",
    "class IntroRoot(FloatLayout):\n",
    "    pass\n",
    "\n",
    "\n",
    "KV = \"\"\"\n",
    "#:import dp kivy.metrics.dp\n",
    "#:import sp kivy.metrics.sp\n",
    "\n",
    "<IntroRoot>:\n",
    "    Label:\n",
    "        id: deck_label\n",
    "        text: \"LIVE GRID 64 / MIDI CLOCK / KIVY CORE\"\n",
    "        size_hint: 0.94, None\n",
    "        height: dp(22)\n",
    "        pos_hint: {\"center_x\": 0.5, \"top\": 0.998}\n",
    "        color: 0.25, 0.95, 1.0, 0.82\n",
    "        font_size: sp(10)\n",
    "        bold: True\n",
    "        halign: \"center\"\n",
    "        valign: \"middle\"\n",
    "        text_size: self.size\n",
    "\n",
    "    BoxLayout:\n",
    "        id: title_band\n",
    "        orientation: \"horizontal\"\n",
    "        padding: dp(4), 0\n",
    "        spacing: dp(2)\n",
    "        size_hint: 0.96, None\n",
    "        height: dp(74)\n",
    "        pos_hint: {\"center_x\": 0.5, \"top\": 0.965}\n",
    "\n",
    "        Label:\n",
    "            id: python_label\n",
    "            text: \"Python\"\n",
    "            color: 0.188, 0.412, 0.596, 1\n",
    "            font_size: sp(32)\n",
    "            bold: True\n",
    "            halign: \"right\"\n",
    "            valign: \"middle\"\n",
    "            text_size: self.size\n",
    "            size_hint_x: 0.42\n",
    "\n",
    "        Label:\n",
    "            id: here_label\n",
    "            text: \"Here\"\n",
    "            color: 1.0, 0.831, 0.231, 1\n",
    "            font_size: sp(32)\n",
    "            bold: True\n",
    "            halign: \"left\"\n",
    "            valign: \"middle\"\n",
    "            text_size: self.size\n",
    "            size_hint_x: 0.30\n",
    "\n",
    "        Label:\n",
    "            id: version_label\n",
    "            text: \"0.2.0\"\n",
    "            color: 0.65, 1.0, 1.0, 1\n",
    "            font_size: sp(18)\n",
    "            bold: True\n",
    "            halign: \"left\"\n",
    "            valign: \"middle\"\n",
    "            text_size: self.size\n",
    "            size_hint_x: 0.28\n",
    "\n",
    "    BoxLayout:\n",
    "        id: feature_panel\n",
    "        orientation: \"vertical\"\n",
    "        padding: dp(12), dp(8)\n",
    "        spacing: dp(4)\n",
    "        size_hint: 0.92, 0.30\n",
    "        pos_hint: {\"center_x\": 0.5, \"center_y\": 0.595}\n",
    "        canvas.before:\n",
    "            Color:\n",
    "                rgba: 0.02, 0.09, 0.12, 0.78\n",
    "            Rectangle:\n",
    "                pos: self.pos\n",
    "                size: self.size\n",
    "            Color:\n",
    "                rgba: 0.0, 0.85, 1.0, 0.28\n",
    "            Line:\n",
    "                rectangle: self.x, self.y, self.width, self.height\n",
    "                width: 1.2\n",
    "\n",
    "        Label:\n",
    "            id: feature_title\n",
    "            text: \"\"\n",
    "            color: 0.70, 1.0, 1.0, 1\n",
    "            font_size: sp(22)\n",
    "            bold: True\n",
    "            halign: \"center\"\n",
    "            valign: \"middle\"\n",
    "            text_size: self.width - dp(18), self.height\n",
    "            shorten: True\n",
    "            shorten_from: \"right\"\n",
    "            size_hint_y: 0.58\n",
    "\n",
    "        Label:\n",
    "            id: feature_subtitle\n",
    "            text: \"\"\n",
    "            color: 1.0, 0.88, 0.42, 1\n",
    "            font_size: sp(15)\n",
    "            halign: \"center\"\n",
    "            valign: \"middle\"\n",
    "            text_size: self.width - dp(18), self.height\n",
    "            shorten: True\n",
    "            shorten_from: \"right\"\n",
    "            size_hint_y: 0.42\n",
    "\n",
    "    BoxLayout:\n",
    "        id: osc_bank\n",
    "        orientation: \"vertical\"\n",
    "        spacing: dp(4)\n",
    "        size_hint: 0.88, None\n",
    "        height: dp(86)\n",
    "        pos_hint: {\"center_x\": 0.5, \"center_y\": 0.315}\n",
    "\n",
    "        Slider:\n",
    "            id: osc_a\n",
    "            min: 0\n",
    "            max: 100\n",
    "            value: 0\n",
    "\n",
    "        Slider:\n",
    "            id: osc_b\n",
    "            min: 0\n",
    "            max: 100\n",
    "            value: 0\n",
    "\n",
    "        Slider:\n",
    "            id: osc_c\n",
    "            min: 0\n",
    "            max: 100\n",
    "            value: 0\n",
    "\n",
    "    BoxLayout:\n",
    "        id: meter_bank\n",
    "        orientation: \"vertical\"\n",
    "        spacing: dp(2)\n",
    "        size_hint: 0.88, None\n",
    "        height: dp(70)\n",
    "        pos_hint: {\"center_x\": 0.5, \"y\": 0.145}\n",
    "\n",
    "    BoxLayout:\n",
    "        id: beat_row\n",
    "        orientation: \"horizontal\"\n",
    "        spacing: dp(2)\n",
    "        size_hint: 0.94, None\n",
    "        height: dp(34)\n",
    "        pos_hint: {\"center_x\": 0.5, \"y\": 0.085}\n",
    "\n",
    "    Label:\n",
    "        id: status_label\n",
    "        text: \"arming sequencer\"\n",
    "        size_hint: 0.96, None\n",
    "        height: dp(24)\n",
    "        pos_hint: {\"center_x\": 0.5, \"y\": 0.048}\n",
    "        color: 0.55, 1.0, 0.84, 0.9\n",
    "        font_size: sp(10)\n",
    "        bold: True\n",
    "        halign: \"center\"\n",
    "        valign: \"middle\"\n",
    "        text_size: self.size\n",
    "        shorten: True\n",
    "        shorten_from: \"right\"\n",
    "\n",
    "    Label:\n",
    "        id: ticker\n",
    "        text: \"\"\n",
    "        size_hint: None, None\n",
    "        height: dp(24)\n",
    "        x: root.width\n",
    "        y: dp(3)\n",
    "        color: 0.20, 1.0, 0.58, 0.92\n",
    "        font_size: sp(10)\n",
    "        bold: True\n",
    "        halign: \"left\"\n",
    "        valign: \"middle\"\n",
    "        text_size: None, self.height\n",
    "\n",
    "IntroRoot:\n",
    "\"\"\"\n",
    "\n",
    "\n",
    "def build_midi_pattern():\n",
    "    roots = [48, 45, 41, 43]\n",
    "    chords = [\n",
    "        [60, 64, 67],\n",
    "        [57, 60, 64],\n",
    "        [53, 57, 60],\n",
    "        [55, 59, 62],\n",
    "    ]\n",
    "    leads = [\n",
    "        [72, 74, 76, 79, 76, 74],\n",
    "        [69, 72, 76, 81, 76, 72],\n",
    "        [65, 69, 72, 77, 72, 69],\n",
    "        [67, 71, 74, 79, 83, 79],\n",
    "    ]\n",
    "    lead_slots = [2, 5, 7, 10, 13, 15]\n",
    "    bass_slots = [0, 3, 6, 8, 11, 14]\n",
    "    pattern = [[] for _ in range(STEPS_PER_LOOP)]\n",
    "\n",
    "    for step in range(STEPS_PER_LOOP):\n",
    "        block = step // BLOCK_STEPS\n",
    "        local = step % BLOCK_STEPS\n",
    "        root_note = roots[block]\n",
    "\n",
    "        if local in (0, 8):\n",
    "            pattern[step].append((9, 36, 108, 1))\n",
    "        if local in (4, 12):\n",
    "            pattern[step].append((9, 38, 96, 1))\n",
    "        if local % 2 == 0:\n",
    "            pattern[step].append((9, 42, 62, 1))\n",
    "        if local == 14:\n",
    "            pattern[step].append((9, 46, 72, 1))\n",
    "\n",
    "        if local in bass_slots:\n",
    "            bass_note = root_note if local in (0, 6, 8, 14) else root_note + 7\n",
    "            pattern[step].append((0, bass_note, 92, 2))\n",
    "\n",
    "        if local in (0, 8):\n",
    "            for chord_note in chords[block]:\n",
    "                pattern[step].append((2, chord_note, 50, 7))\n",
    "\n",
    "        if local in lead_slots:\n",
    "            lead_index = lead_slots.index(local)\n",
    "            pattern[step].append((1, leads[block][lead_index], 78, 2))\n",
    "\n",
    "    return pattern\n",
    "\n",
    "\n",
    "class PythonHereIntroController:\n",
    "    def __init__(self, ui, ticker_text, cards):\n",
    "        self.ui = ui\n",
    "        self.cards = cards\n",
    "        self.ticker_text = ticker_text\n",
    "        self.step = 0\n",
    "        self.last_played_step = 0\n",
    "        self.last_tick_time = time.perf_counter()\n",
    "        self.running = False\n",
    "        self.midi_enabled = True\n",
    "        self.midi_error = None\n",
    "        self.active_notes = []\n",
    "        self.used_channels = {0, 1, 2, 9}\n",
    "        self.midi_event = None\n",
    "        self.visual_event = None\n",
    "        self.current_feature_index = None\n",
    "        self.current_version_text = None\n",
    "        self.note_pattern = build_midi_pattern()\n",
    "        self.beat_buttons = []\n",
    "        self.meters = []\n",
    "        self.particle_items = []\n",
    "        self.grid_lines = []\n",
    "        self.scan_color = None\n",
    "        self.scan_rect = None\n",
    "        self.wave_color = None\n",
    "        self.wave_line = None\n",
    "        self.bg_rect = None\n",
    "        self.bg_color = None\n",
    "        self._setup_widgets()\n",
    "        self._setup_canvas()\n",
    "        self.ui.bind(pos=self._resize_canvas, size=self._resize_canvas)\n",
    "        self._resize_canvas()\n",
    "\n",
    "    def _setup_widgets(self):\n",
    "        self.ui.ids.ticker.text = self.ticker_text\n",
    "        self.ui.ids.ticker.texture_update()\n",
    "        self.ui._ticker_width = max(1, int(self.ui.ids.ticker.texture_size[0]))\n",
    "        self.ui.ids.ticker.width = self.ui._ticker_width\n",
    "\n",
    "        for index in range(16):\n",
    "            btn = Button(\n",
    "                text=f\"{index:02d}\",\n",
    "                font_size=sp(9),\n",
    "                bold=True,\n",
    "                background_normal=\"\",\n",
    "                background_down=\"\",\n",
    "                color=(0.65, 1.0, 1.0, 0.85),\n",
    "            )\n",
    "            btn.background_color = (0.02, 0.12, 0.16, 0.70)\n",
    "            self.ui.ids.beat_row.add_widget(btn)\n",
    "            self.beat_buttons.append(btn)\n",
    "\n",
    "        for index in range(8):\n",
    "            meter = ProgressBar(max=100, value=0)\n",
    "            self.ui.ids.meter_bank.add_widget(meter)\n",
    "            self.meters.append(meter)\n",
    "\n",
    "    def _setup_canvas(self):\n",
    "        with self.ui.canvas.before:\n",
    "            self.bg_color = Color(0.005, 0.008, 0.018, 1.0)\n",
    "            self.bg_rect = Rectangle(pos=self.ui.pos, size=self.ui.size)\n",
    "\n",
    "            for _ in range(10):\n",
    "                color = Color(0.0, 0.45, 0.58, 0.10)\n",
    "                line = Line(points=[0, 0, 0, 0], width=0.8)\n",
    "                self.grid_lines.append((color, line))\n",
    "\n",
    "            self.wave_color = Color(0.0, 0.95, 1.0, 0.52)\n",
    "            self.wave_line = Line(points=[], width=1.4)\n",
    "\n",
    "            self.scan_color = Color(0.2, 1.0, 0.75, 0.10)\n",
    "            self.scan_rect = Rectangle(pos=(0, 0), size=(1, dp(3)))\n",
    "\n",
    "            for i in range(28):\n",
    "                color = Color(0.20, 1.0, 0.75, 0.0)\n",
    "                dot = Ellipse(pos=(0, 0), size=(dp(2), dp(2)))\n",
    "                self.particle_items.append((color, dot, i))\n",
    "\n",
    "    def _resize_canvas(self, *args):\n",
    "        w = max(1.0, float(self.ui.width))\n",
    "        h = max(1.0, float(self.ui.height))\n",
    "        self.bg_rect.pos = self.ui.pos\n",
    "        self.bg_rect.size = self.ui.size\n",
    "\n",
    "        for i, pair in enumerate(self.grid_lines):\n",
    "            color, line = pair\n",
    "            if i < 5:\n",
    "                x = w * (i + 1) / 6.0\n",
    "                line.points = [x, 0, x, h]\n",
    "            else:\n",
    "                y = h * (i - 4) / 6.0\n",
    "                line.points = [0, y, w, y]\n",
    "            color.rgba = (0.0, 0.55, 0.70, 0.08)\n",
    "\n",
    "    def _program_synth(self):\n",
    "        global midistream_used_channels\n",
    "        synth = get_synthesizer()\n",
    "        synth.volume = 86\n",
    "        synth.reverb = ReverbPreset.ROOM\n",
    "        setup = []\n",
    "        setup += midi_program_change(38, channel=0)\n",
    "        setup += midi_program_change(81, channel=1)\n",
    "        setup += midi_program_change(88, channel=2)\n",
    "        setup += midi_control_change(Control.volume, 104, channel=0)\n",
    "        setup += midi_control_change(Control.volume, 92, channel=1)\n",
    "        setup += midi_control_change(Control.volume, 70, channel=2)\n",
    "        setup += midi_control_change(Control.pan, 44, channel=0)\n",
    "        setup += midi_control_change(Control.pan, 82, channel=1)\n",
    "        setup += midi_control_change(Control.pan, 64, channel=2)\n",
    "        synth.write(setup)\n",
    "        midistream_used_channels.update(self.used_channels)\n",
    "\n",
    "    def _safe_note_off_list(self, notes):\n",
    "        global midistream_active_notes\n",
    "        if not notes or not self.midi_enabled:\n",
    "            return\n",
    "        cmd = []\n",
    "        for channel, note in notes:\n",
    "            cmd += midi_note_off(note, channel=channel, velocity=0)\n",
    "        get_synthesizer().write(cmd)\n",
    "        for channel, note in notes:\n",
    "            midistream_active_notes.discard((channel, note))\n",
    "\n",
    "    def all_sound_off(self):\n",
    "        global midistream_active_notes\n",
    "        if not self.midi_enabled:\n",
    "            self.active_notes = []\n",
    "            midistream_active_notes.clear()\n",
    "            return\n",
    "        try:\n",
    "            off_notes = [(channel, note) for channel, note, _remaining in self.active_notes]\n",
    "            if off_notes:\n",
    "                self._safe_note_off_list(off_notes)\n",
    "            cmd = []\n",
    "            for channel in sorted(self.used_channels):\n",
    "                cmd += midi_control_change(Control.all_sound_off, 0, channel=channel)\n",
    "            get_synthesizer().write(cmd)\n",
    "        except Exception:\n",
    "            Logger.exception(\"PythonHere: Could not silence intro MIDI\")\n",
    "        self.active_notes = []\n",
    "        midistream_active_notes.clear()\n",
    "\n",
    "    def start(self):\n",
    "        pythonhere_intro_state.update(\n",
    "            ok=True,\n",
    "            stage=\"running\",\n",
    "            message=\"PythonHere 0.2.x intro running\",\n",
    "            error=None,\n",
    "        )\n",
    "        self.running = True\n",
    "        try:\n",
    "            self._program_synth()\n",
    "            self.ui.ids.status_label.text = \"MIDI locked / 64 step loop / visual clock slaved\"\n",
    "        except Exception as exc:\n",
    "            self.midi_enabled = False\n",
    "            self.midi_error = f\"{type(exc).__name__}: {exc}\"\n",
    "            pythonhere_intro_state.update(\n",
    "                ok=False,\n",
    "                stage=\"midi_init\",\n",
    "                message=\"Visual intro running without MIDI\",\n",
    "                error=self.midi_error,\n",
    "            )\n",
    "            self.ui.ids.status_label.text = \"MIDI init failed / visual clock continues\"\n",
    "            Logger.exception(\"PythonHere: Could not initialize MIDI intro\")\n",
    "\n",
    "        self.visual_tick(0)\n",
    "        self.midi_tick(0)\n",
    "        self.midi_event = Clock.schedule_interval(self.midi_tick, TICK_SECONDS)\n",
    "        self.visual_event = Clock.schedule_interval(self.visual_tick, 1.0 / VISUAL_FPS)\n",
    "\n",
    "    def stop(self, close_midi=False):\n",
    "        self.running = False\n",
    "        if self.midi_event is not None:\n",
    "            self.midi_event.cancel()\n",
    "            self.midi_event = None\n",
    "        if self.visual_event is not None:\n",
    "            self.visual_event.cancel()\n",
    "            self.visual_event = None\n",
    "        self.all_sound_off()\n",
    "        if close_midi:\n",
    "            try:\n",
    "                synth = globals().get(\"synthesizer\")\n",
    "                if synth is not None:\n",
    "                    synth.close()\n",
    "                globals()[\"synthesizer\"] = None\n",
    "            except Exception:\n",
    "                Logger.exception(\"PythonHere: Could not close synthesizer\")\n",
    "        pythonhere_intro_state.update(\n",
    "            ok=True,\n",
    "            stage=\"stopped\",\n",
    "            message=\"PythonHere intro stopped\",\n",
    "            error=None,\n",
    "        )\n",
    "\n",
    "    def _loop_position(self):\n",
    "        elapsed_since_tick = max(0.0, time.perf_counter() - self.last_tick_time)\n",
    "        micro = min(0.999, elapsed_since_tick / TICK_SECONDS)\n",
    "\n",
    "        step_float = self.last_played_step + micro\n",
    "        step_float %= STEPS_PER_LOOP\n",
    "\n",
    "        step = int(step_float)\n",
    "        phase = step_float / STEPS_PER_LOOP\n",
    "\n",
    "        return step, micro, phase\n",
    "\n",
    "    def midi_tick(self, dt):\n",
    "        global midistream_active_notes, midistream_used_channels\n",
    "        if not self.running:\n",
    "            return False\n",
    "\n",
    "        played_step = self.step\n",
    "\n",
    "        if self.midi_enabled:\n",
    "            try:\n",
    "                expired = []\n",
    "                survivors = []\n",
    "                for channel, note, remaining_steps in self.active_notes:\n",
    "                    remaining_steps -= 1\n",
    "                    if remaining_steps <= 0:\n",
    "                        expired.append((channel, note))\n",
    "                    else:\n",
    "                        survivors.append((channel, note, remaining_steps))\n",
    "                self.active_notes = survivors\n",
    "\n",
    "                if expired:\n",
    "                    self._safe_note_off_list(expired)\n",
    "\n",
    "                events = self.note_pattern[played_step]\n",
    "                if events:\n",
    "                    cmd = []\n",
    "                    for channel, note, velocity, _duration in events:\n",
    "                        cmd += midi_note_on(note, channel=channel, velocity=velocity)\n",
    "                    get_synthesizer().write(cmd)\n",
    "\n",
    "                    for channel, note, _velocity, duration in events:\n",
    "                        self.active_notes.append((channel, note, duration))\n",
    "                        midistream_active_notes.add((channel, note))\n",
    "                        midistream_used_channels.add(channel)\n",
    "                        self.used_channels.add(channel)\n",
    "\n",
    "            except MIDIException as exc:\n",
    "                self.midi_enabled = False\n",
    "                self.midi_error = f\"{type(exc).__name__}: {exc}\"\n",
    "                pythonhere_intro_state.update(\n",
    "                    ok=False,\n",
    "                    stage=\"midi_tick\",\n",
    "                    message=\"MIDI disabled after sequencer error\",\n",
    "                    error=self.midi_error,\n",
    "                )\n",
    "                self.ui.ids.status_label.text = \"MIDI error / visual sequencer still loops\"\n",
    "                Logger.exception(\"PythonHere: MIDI intro tick failed\")\n",
    "            except Exception as exc:\n",
    "                self.midi_enabled = False\n",
    "                self.midi_error = f\"{type(exc).__name__}: {exc}\"\n",
    "                pythonhere_intro_state.update(\n",
    "                    ok=False,\n",
    "                    stage=\"midi_tick\",\n",
    "                    message=\"MIDI disabled after sequencer error\",\n",
    "                    error=self.midi_error,\n",
    "                )\n",
    "                self.ui.ids.status_label.text = \"MIDI error / visual sequencer still loops\"\n",
    "                Logger.exception(\"PythonHere: MIDI intro tick failed\")\n",
    "\n",
    "        self.last_played_step = played_step\n",
    "        self.last_tick_time = time.perf_counter()\n",
    "        self.step = (self.step + 1) % STEPS_PER_LOOP\n",
    "        pythonhere_intro_state[\"step\"] = played_step\n",
    "        pythonhere_intro_state[\"next_step\"] = self.step\n",
    "        return True\n",
    "\n",
    "    def visual_tick(self, dt):\n",
    "        if not self.running:\n",
    "            return False\n",
    "\n",
    "        visual_step, micro, phase = self._loop_position()\n",
    "\n",
    "        feature_index = min(\n",
    "            len(self.cards) - 1,\n",
    "            visual_step // FEATURE_STEPS,\n",
    "        )\n",
    "\n",
    "        local_feature_phase = (\n",
    "            (visual_step % FEATURE_STEPS) + micro\n",
    "        ) / FEATURE_STEPS\n",
    "\n",
    "        active16 = visual_step % BLOCK_STEPS\n",
    "\n",
    "        version_char = [\"0\", \"1\", \"2\", \"x\"][\n",
    "            (visual_step % BLOCK_STEPS) // 4\n",
    "        ]\n",
    "        version_text = \"0.2.\" + version_char\n",
    "\n",
    "        block = visual_step // BLOCK_STEPS\n",
    "        local = visual_step % BLOCK_STEPS\n",
    "\n",
    "        if feature_index != self.current_feature_index:\n",
    "            card = self.cards[feature_index]\n",
    "            self.ui.ids.feature_title.text = card[\"title\"]\n",
    "            self.ui.ids.feature_subtitle.text = card[\"subtitle\"]\n",
    "            title_len = len(card[\"title\"])\n",
    "            self.ui.ids.feature_title.font_size = sp(18 if title_len > 28 else 22)\n",
    "            self.current_feature_index = feature_index\n",
    "\n",
    "        if version_text != self.current_version_text:\n",
    "            self.ui.ids.version_label.text = version_text\n",
    "            self.current_version_text = version_text\n",
    "\n",
    "        fade_in = min(1.0, local_feature_phase * 5.5)\n",
    "        fade_out = min(1.0, (1.0 - local_feature_phase) * 5.5)\n",
    "        panel_alpha = max(0.0, min(fade_in, fade_out))\n",
    "        pulse = 0.5 + 0.5 * math.sin(2.0 * math.pi * ((active16 + micro) / BLOCK_STEPS))\n",
    "\n",
    "        self.ui.ids.feature_panel.opacity = 0.62 + 0.38 * panel_alpha\n",
    "        self.ui.ids.python_label.opacity = 0.82 + 0.18 * pulse\n",
    "        self.ui.ids.here_label.opacity = 0.82 + 0.18 * (1.0 - pulse)\n",
    "        self.ui.ids.version_label.color = (\n",
    "            0.72 + 0.20 * pulse,\n",
    "            1.0,\n",
    "            1.0,\n",
    "            1.0,\n",
    "        )\n",
    "\n",
    "        for i, btn in enumerate(self.beat_buttons):\n",
    "            distance = min((i - active16) % BLOCK_STEPS, (active16 - i) % BLOCK_STEPS)\n",
    "            intensity = max(0.0, 1.0 - distance / 5.0)\n",
    "            if i == active16:\n",
    "                btn.background_color = (0.05, 0.95, 1.0, 0.98)\n",
    "                btn.color = (0.0, 0.02, 0.04, 1.0)\n",
    "            elif i % 4 == 0:\n",
    "                btn.background_color = (0.18, 0.34 + 0.24 * intensity, 0.62, 0.78)\n",
    "                btn.color = (0.78, 1.0, 1.0, 0.82)\n",
    "            else:\n",
    "                btn.background_color = (0.02, 0.11 + 0.22 * intensity, 0.16 + 0.28 * intensity, 0.70)\n",
    "                btn.color = (0.42, 0.95, 1.0, 0.74)\n",
    "\n",
    "        for i, meter in enumerate(self.meters):\n",
    "            meter.value = 8 + 86 * abs(math.sin(2.0 * math.pi * (phase * (i + 1) + i * 0.091)))\n",
    "\n",
    "        self.ui.ids.osc_a.value = 50 + 48 * math.sin(2.0 * math.pi * (phase * 4.0))\n",
    "        self.ui.ids.osc_b.value = 50 + 48 * math.sin(2.0 * math.pi * (phase * 8.0 + 0.18))\n",
    "        self.ui.ids.osc_c.value = 50 + 48 * math.sin(2.0 * math.pi * (phase * 16.0 + 0.37))\n",
    "\n",
    "        w = max(1.0, float(self.ui.width))\n",
    "        h = max(1.0, float(self.ui.height))\n",
    "\n",
    "        self.ui.ids.ticker.x = self.ui.width - phase * (self.ui.width + self.ui._ticker_width)\n",
    "\n",
    "        scan_y = ((visual_step + micro) / STEPS_PER_LOOP) * h\n",
    "        self.scan_rect.pos = (0, scan_y)\n",
    "        self.scan_rect.size = (w, dp(3))\n",
    "        self.scan_color.rgba = (0.0, 1.0, 0.72, 0.06 + 0.08 * pulse)\n",
    "\n",
    "        wave_points = []\n",
    "        base_y = h * 0.455\n",
    "        amp = h * 0.025\n",
    "        for i in range(32):\n",
    "            x = w * i / 31.0\n",
    "            y = base_y + amp * math.sin(2.0 * math.pi * (phase * 8.0 + i / 7.0))\n",
    "            wave_points.extend([x, y])\n",
    "        self.wave_line.points = wave_points\n",
    "        self.wave_color.rgba = (0.0, 0.82 + 0.18 * pulse, 1.0, 0.34 + 0.22 * pulse)\n",
    "\n",
    "        for color, dot, i in self.particle_items:\n",
    "            px = (w * ((i * 37) % 101) / 100.0 + phase * w * (0.25 + (i % 5) * 0.06)) % w\n",
    "            py = h * (0.20 + 0.62 * (((i * 19) % 97) / 96.0))\n",
    "            py += math.sin(2.0 * math.pi * (phase * (1 + (i % 4)) + i * 0.13)) * h * 0.018\n",
    "            size = dp(1.6 + (i % 4))\n",
    "            dot.pos = (px, py)\n",
    "            dot.size = (size, size)\n",
    "            color.rgba = (\n",
    "                0.18 + 0.12 * (i % 3),\n",
    "                0.78 + 0.22 * pulse,\n",
    "                1.0,\n",
    "                0.18 + 0.22 * ((i + active16) % 4 == 0),\n",
    "            )\n",
    "\n",
    "        self.ui.ids.status_label.text = (\n",
    "            f\"STEP {visual_step:02d}/63  BLOCK {block + 1}/4  LOCAL {local:02d}  \"\n",
    "            f\"FEATURE {feature_index + 1}/8\"\n",
    "        )\n",
    "\n",
    "        pythonhere_intro_state[\"visual_step\"] = visual_step\n",
    "        pythonhere_intro_state[\"phase\"] = phase\n",
    "        pythonhere_intro_state[\"feature_index\"] = feature_index\n",
    "        pythonhere_intro_state[\"version\"] = version_text\n",
    "\n",
    "        return True\n",
    "\n",
    "\n",
    "def stop_pythonhere_intro(close_midi=False):\n",
    "    controller = globals().get(\"pythonhere_intro_controller\")\n",
    "    if controller is not None:\n",
    "        controller.stop(close_midi=close_midi)\n",
    "\n",
    "\n",
    "try:\n",
    "    pythonhere_intro_ui = Builder.load_string(KV)\n",
    "    if pythonhere_intro_ui is None:\n",
    "        raise RuntimeError(\"Builder.load_string(KV) returned None\")\n",
    "\n",
    "    pythonhere_intro_controller = PythonHereIntroController(\n",
    "        pythonhere_intro_ui,\n",
    "        pythonhere_intro_ticker_text,\n",
    "        announcements,\n",
    "    )\n",
    "\n",
    "    root.clear_widgets()\n",
    "    root.add_widget(pythonhere_intro_ui)\n",
    "\n",
    "    pythonhere_intro_controller.start()\n",
    "\n",
    "\n",
    "except Exception as exc:\n",
    "    pythonhere_last_error = f\"{type(exc).__name__}: {exc}\"\n",
    "    pythonhere_intro_state.update(\n",
    "        ok=False,\n",
    "        stage=\"setup_failed\",\n",
    "        message=\"Could not start PythonHere 0.2.x intro\",\n",
    "        error=pythonhere_last_error,\n",
    "    )\n",
    "    Logger.exception(\"PythonHere: Could not start PythonHere 0.2.x intro\")\n",
    "    Popup(\n",
    "        title=\"PythonHere intro error\",\n",
    "        content=Label(\n",
    "            text=pythonhere_last_error,\n",
    "            text_size=(dp(280), None),\n",
    "            halign=\"center\",\n",
    "            valign=\"middle\",\n",
    "        ),\n",
    "        size_hint=(0.86, 0.34),\n",
    "    ).open()"
   ]
  }
 ],
 "metadata": {
  "jupytext": {
   "default_lexer": "ipython3"
  },
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
