Changeset - 1dc96b97a544
[Not reviewed]
default
0 5 0
drewp@bigasterisk.com - 3 years ago 2022-06-03 07:41:13
drewp@bigasterisk.com
two zoomed spectrogram views in asco
5 files changed with 95 insertions and 49 deletions:
0 comments (0 inline, 0 general)
light9/ascoltami/Light9AscoltamiUi.ts
Show inline comments
 
import debug from "debug";
 
import { html, LitElement } from "lit";
 
import { css, html, LitElement } from "lit";
 
import { customElement, property } from "lit/decorators.js";
 
import { NamedNode } from "n3";
 
import { getTopGraph } from "../web/RdfdbSyncedGraph";
 
import { SyncedGraph } from "../web/SyncedGraph";
 
export { RdfdbSyncedGraph } from "../web/RdfdbSyncedGraph";
 
export { Light9TimelineAudio } from "../web/light9-timeline-audio";
 
import { classMap } from "lit/directives/class-map.js";
 
import { TimingUpdate } from "./main";
 
import { Zoom } from "../web/light9-timeline-audio";
 

	
 
debug.enable("*");
 
const log = debug("asco");
 

	
 
function byId(id: string): HTMLElement {
 
  return document.getElementById(id)!;
 
}
 
async function postJson(url: string, jsBody: Object) {
 
  return fetch(url, {
 
    method: "POST",
 
    headers: { "Content-Type": "applcation/json" },
 
    body: JSON.stringify(jsBody),
 
  });
 
}
 
@customElement("light9-ascoltami-ui")
 
export class Light9AscoltamiUi extends LitElement {
 
  graph!: SyncedGraph;
 
  times!: { intro: number; post: number };
 
  @property() nextText: string = "";
 
  @property() isPlaying: boolean = false;
 
  @property() show: NamedNode | null = null;
 
  @property() song: NamedNode | null = null;
 
  @property() t: number = 0;
 
  @property() currentDuration: number = 0;
 
  @property() zoom: Zoom;
 
  @property() overviewZoom: Zoom;
 
  static styles = [
 
    css`
 
      .timeRow {
 
        margin: 14px;
 
      }
 
      light9-timeline-audio {
 
        height: 80px;
 
      }
 
    `,
 
  ];
 
  render() {
 
    return html`<rdfdb-synced-graph></rdfdb-synced-graph>
 

	
 
      <link rel="stylesheet" href="./style.css" />
 

	
 
      <h1>ascoltami <a href="metrics">[metrics]</a></h1>
 

	
 
      <div class="timeRow">
 
        <div id="timeSlider"></div>
 
        <light9-timeline-audio .show=${this.show} .song=${this.song} .zoom=${this.overviewZoom}></light9-timeline-audio>
 
        <light9-timeline-audio .show=${this.show} .song=${this.song} .zoom=${this.zoom}></light9-timeline-audio>
 
      </div>
 

	
 
      <div class="commands">
 
        <button id="cmd-stop" @click=${this.onCmdStop} class="playMode ${classMap({ active: !this.isPlaying })}">
 
          <strong>Stop</strong>
 
          <div class="key">s</div>
 
        </button>
 
        <button id="cmd-play" @click=${this.onCmdPlay} class="playMode ${classMap({ active: this.isPlaying })}">
 
          <strong>Play</strong>
 
          <div class="key">p</div>
 
        </button>
 
        <button id="cmd-intro" @click=${this.onCmdIntro}>
 
          <strong>Skip intro</strong>
 
          <div class="key">i</div>
 
@@ -97,51 +117,50 @@ export class Light9AscoltamiUi extends L
 
        this.onCmdPost();
 
        return false;
 
      }
 

	
 
      if (ev.key == "g") {
 
        this.onCmdGo();
 
        return false;
 
      }
 
      return true;
 
    });
 
  }
 

	
 
  currentDurationChanged(newDuration: number): void {
 
    this.currentDuration = newDuration;
 
  }
 

	
 
  async musicSetup() {
 
    // shoveled over from the vanillajs version
 
    const config = await (await fetch("api/config")).json();
 
    this.show = new NamedNode(config.show);
 
    this.times = config.times;
 
    document.title = document.title.replace("{{host}}", config.host);
 
    const h1 = document.querySelector("h1")!;
 
    h1.innerText = h1.innerText.replace("{{host}}", config.host);
 

	
 
    byId("nav").innerText = navigator.userAgent;
 
    var updateFreq = navigator.userAgent.indexOf("Linux") != -1 ? 10 : 2;
 
    if (navigator.userAgent.match(/Windows NT/)) {
 
      // helper laptop
 
      updateFreq = 10;
 
    }
 
    byId("updateReq").innerText = "" + updateFreq;
 

	
 
    (window as any).finishOldStyleSetup(this.times, updateFreq, (data: TimingUpdate) => {
 
      this.nextText = data.next;
 
      this.isPlaying = data.playing;
 
      this.currentDuration = data.duration;
 
      this.song = new NamedNode(data.song);
 
      this.overviewZoom = { duration: data.duration, t1: 0, t2: data.duration };
 
      this.zoom = { duration: data.duration, t1: data.t - 2, t2: data.t + 20 };
 
    });
 
  }
 

	
 
  constructor() {
 
    super();
 
    this.bindKeys();
 
    //   byId("cmd-stop").addEventListener("click", (ev: Event) =>
 
    // );
 
    this.zoom = this.overviewZoom = { duration: null, t1: 0, t2: 1 };
 

	
 
    getTopGraph().then((g) => {
 
      this.graph = g;
 
      this.musicSetup(); // async
 
    });
 
  }
 
}
light9/ascoltami/index.html
Show inline comments
 
@@ -27,27 +27,24 @@
 
          <td><strong>Time:</strong> <span id="currentTime"></span></td>
 
          <td><strong>Left:</strong> <span id="leftTime"></span></td>
 
          <td><strong>Until autostop:</strong> <span id="leftAutoStopTime"></span></td>
 
        </tr>
 
        <tr>
 
          <td colspan="3">
 
            <strong>Update freq:</strong> requested <span id="updateReq"></span>, actual <span id="updateActual"></span> <strong>States:</strong>
 
            <span id="states"></span>
 
          </td>
 
        </tr>
 
      </table>
 

	
 
      <div class="timeRow">
 
        <div id="timeSlider"></div>
 
      </div>
 
    </div>
 

	
 
    <hr />
 
    new ui is here
 
    <light9-ascoltami-ui></light9-ascoltami-ui>
 
    <hr />
 

	
 
    <p>Running on <span id="nav"></span></p>
 
    <p><a href="">reload</a></p>
 

	
 
    <script type="module" src="../ascoltami/main.ts"></script>
 
  </body>
light9/ascoltami/webapp.py
Show inline comments
 
import json
 
import logging
 
import socket
 
import subprocess
 
import time
 
from typing import cast
 

	
 
import cyclone.web
 
import cyclone.websocket
 
from cycloneerr import PrettyErrorHandler
 
from light9.metrics import metricsRoute
 
from light9.namespaces import L9
 
from light9.showconfig import getSongsFromShow, songOnDisk
 
from light9.showconfig import getSongsFromShow, showUri, songOnDisk
 
from rdflib import URIRef
 
from twisted.internet import reactor
 
from twisted.internet.interfaces import IReactorTime
 

	
 
log = logging.getLogger()
 
_songUris = {}  # locationUri : song
 

	
 

	
 
def songLocation(graph, songUri):
 
    loc = URIRef("file://%s" % songOnDisk(songUri))
 
    _songUris[loc] = songUri
 
    return loc
 
@@ -27,24 +27,25 @@ def songLocation(graph, songUri):
 

	
 
def songUri(graph, locationUri):
 
    return _songUris[locationUri]
 

	
 

	
 
class config(cyclone.web.RequestHandler):
 

	
 
    def get(self):
 
        self.set_header("Content-Type", "application/json")
 
        self.write(
 
            json.dumps(dict(
 
                host=socket.gethostname(),
 
                show=str(showUri()),
 
                times={
 
                    # these are just for the web display. True values are on Player.__init__
 
                    'intro': 4,
 
                    'post': 0
 
                })))
 

	
 

	
 
def playerSongUri(graph, player):
 
    """or None"""
 

	
 
    playingLocation = player.getSong()
 
    if playingLocation:
light9/web/light9-timeline-audio.ts
Show inline comments
 
import { debug } from "debug";
 

	
 
import { css, html, LitElement, TemplateResult } from "lit";
 
import { customElement, property } from "lit/decorators.js";
 
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
 
import { customElement, property, state } from "lit/decorators.js";
 
import { NamedNode } from "n3";
 
import { loadConfigFromFile } from "vite";
 
import { getTopGraph } from "./RdfdbSyncedGraph";
 
import { SyncedGraph } from "./SyncedGraph";
 

	
 
const log = debug("audio");
 

	
 
export interface Zoom {
 
  duration: number | null;
 
  t1: number;
 
  t2: number;
 
}
 

	
 
function nodeHasChanged(newVal?: NamedNode, oldVal?: NamedNode): boolean {
 
  if (newVal === undefined && oldVal === undefined) {
 
    return false;
 
  }
 
  if (newVal === undefined || oldVal === undefined) {
 
    return true;
 
  }
 
  return !newVal.equals(oldVal);
 
}
 

	
 
// (potentially-zoomed) spectrogram view
 
@customElement("light9-timeline-audio")
 
export class Light9TimelineAudio extends LitElement {
 
  graph!: SyncedGraph;
 
  render() {
 
    return html`
 
      <style>
 
        :host {
 
          display: block;
 
          /* shouldn't be seen, but black is correct for 'no
 
         audio'. Maybe loading stripes would be better */
 
          background: #202322;
 
        }
 
        div {
 
          width: 100%;
 
          height: 100%;
 
          overflow: hidden;
 
        }
 
        img {
 
          height: 100%;
 
          position: relative;
 
          transition: left .1s linear;
 

	
 
        }
 
      </style>
 
      <div>
 
        <img src="{{imgSrc}}" style="width: {{imgWidth}} ; left: {{imgLeft}}" />
 
        <img src=${this.imgSrc} style="width: ${this.imgWidth}; left: ${this.imgLeft}" />
 
      </div>
 
    `;
 
  }
 
  //    properties= {
 
  //        graph: {type: Object, notify: true},
 
  //        show: {type: String, notify: true},
 
  //        song: {type: String, notify: true},
 
  //        zoom: {type: Object, notify: true},
 
  //        imgSrc: { type: String, notify: true},
 
  //        imgWidth: { computed: '_imgWidth(zoom)' },
 
  //        imgLeft: { computed: '_imgLeft(zoom)' },
 
  //    }
 
  //    observers= [
 
  //        'setImgSrc(graph, show, song)'
 
  //    ]
 
  ready() {
 
    this.zoom = { duration: 0 };
 
  @property({ hasChanged: nodeHasChanged }) show!: NamedNode;
 
  @property({ hasChanged: nodeHasChanged }) song!: NamedNode;
 
  @property() zoom: Zoom = { duration: null, t1: 0, t2: 1 };
 
  @state() imgSrc: string = "#";
 
  @state() imgWidth: string = "0"; // css
 
  @state() imgLeft: string = "0"; // css
 

	
 
  constructor() {
 
    super();
 

	
 
    getTopGraph().then((g) => {
 
      this.graph = g;
 
    });
 
  }
 

	
 
  updated(changedProperties: PropertyValues) {
 
    if (changedProperties.has("song") || changedProperties.has("show")) {
 
      if (this.song && this.show) {
 
        this.graph.runHandler(this.setImgSrc.bind(this), "timeline-audio " + this.song);
 
      }
 
    }
 
    if (changedProperties.has("zoom")) {
 
      this.imgWidth = this._imgWidth(this.zoom);
 
      this.imgLeft = this._imgLeft(this.zoom);
 
    }
 
  }
 

	
 
  setImgSrc() {
 
    graph.runHandler(
 
      function () {
 
        try {
 
          var root = this.graph.stringValue(this.graph.Uri(this.show), this.graph.Uri(":spectrogramUrlRoot"));
 
        } catch (e) {
 
          return;
 
        }
 
    try {
 
      var root = this.graph.stringValue(this.show, this.graph.Uri(":spectrogramUrlRoot"));
 
    } catch (e) {
 
      return;
 
    }
 

	
 
        try {
 
          var filename = this.graph.stringValue(this.song, this.graph.Uri(":songFilename"));
 
        } catch (e) {
 
          return;
 
        }
 
    try {
 
      var filename = this.graph.stringValue(this.song, this.graph.Uri(":songFilename"));
 
    } catch (e) {
 
      return;
 
    }
 

	
 
        this.imgSrc = root + "/" + filename.replace(".wav", ".png").replace(".ogg", ".png");
 
      }.bind(this),
 
      "timeline-audio " + this.song
 
    );
 
    this.imgSrc = root + "/" + filename.replace(".wav", ".png").replace(".ogg", ".png");
 
    log(`imgSrc ${this.imgSrc}`);
 
  }
 
  _imgWidth(zoom) {
 

	
 
  _imgWidth(zoom: Zoom): string {
 
    if (!zoom.duration) {
 
      return "100%";
 
    }
 

	
 
    return 100 / ((zoom.t2 - zoom.t1) / zoom.duration) + "%";
 
  }
 
  _imgLeft(zoom) {
 
  _imgLeft(zoom: Zoom): string {
 
    if (!zoom.duration) {
 
      return "0";
 
    }
 

	
 
    var percentPerSec = 100 / (zoom.t2 - zoom.t1);
 
    return -percentPerSec * zoom.t1 + "%";
 
  }
 
}
light9/web/style.css
Show inline comments
 
@@ -89,27 +89,24 @@ div.keys {
 
  border: 1px outset #b3b3b3;
 
  padding: 2px 3px;
 
  margin: 3px 0;
 
  font-size: 16px;
 
  box-shadow: 0.9px 0.9px 0px 2px #565656;
 
  border-radius: 2px;
 
}
 

	
 
.currentSong button {
 
  background: #a90707;
 
}
 

	
 
.timeRow {
 
  margin: 14px;
 
}
 

	
 
.stalled {
 
  opacity: 0.5;
 
}
 

	
 
.num {
 
  font-size: 27px;
 
  color: rgb(233, 122, 122);
 
  display: inline-block;
 
  font-size: 200% !important;
 
  font-weight: bold;
 
  text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000,
0 comments (0 inline, 0 general)