rAVEUK
/

File size: 7,203 Bytes
cecd352
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
"use strict";

// These display adaptations leave the saved ABC unchanged.
function expandRests(abc) {
  let header = true, voice = null, meter = [4, 4], unit = [1, 8];
  const meters = new Map();
  return abc.split(/\r?\n/).map(line => {
    const field = line.match(/^([A-Za-z]):\s*(.*)$/);
    if (field) {
      const [, key, value] = field;
      if (key === "V") {
        voice = value.split(/\s/)[0];
        if (!meters.has(voice)) meters.set(voice, meter);
      }
      if (key === "M") {
        const match = value.match(/^(\d+)\/(\d+)$/);
        if (match) {
          const next = match.slice(1).map(Number);
          if (header || !voice) meter = next;
          else meters.set(voice, next);
        }
      }
      if (key === "L") unit = value.split("/").map(Number);
      if (key === "K") header = false;
      return line;
    }
    if (header || line.startsWith("%")) return line;
    return line.replace(/"[^"\n]*"|\[[^\]\n]*\]|Z(\d*)\|/g, (token, count) => {
      if (count === undefined) return token;
      const [n, d] = meters.get(voice) || meter;
      return (`z${n * unit[1] / (d * unit[0])}|`).repeat(Number(count || 1));
    });
  }).join("\n");
}

function fixSystemKeys(tune) {
  for (const line of tune.lines) for (const staff of line.staff || []) {
    for (const voice of staff.voices) {
      for (let i = 0; i < voice.length && voice[i].el_type !== "note";) {
        const event = voice[i];
        if (event.el_type === "key" || event.el_type === "keySignature") {
          staff.key = {...event, accidentals: event.accidentals.map(a => ({...a}))};
          delete staff.key.impliedNaturals;
          voice.splice(i, 1);
        } else i++;
      }
    }
  }
}

window.renderScore = async abc => {
  document.body.innerHTML = '<div id="source"></div><main id="pages"></main>';
  const fontStyle = document.createElement("style");
  fontStyle.textContent = '#source text, #source span, #pages text {font-family:SheetSageSans !important}';
  document.head.append(fontStyle);
  const source = document.getElementById("source");
  source.style.cssText = "position:absolute;left:-2000px;width:714px";
  const tunes = ABCJS.renderAbc(source, expandRests(abc), {
    staffwidth: 674, paddingtop: 12, paddingbottom: 12, paddingleft: 20, paddingright: 20,
    oneSvgPerLine: true, print: true, add_classes: true,
    wrap: {minSpacing: 1.8, maxSpacing: 2.7, preferredMeasuresPerLine: 4},
    afterParsing: fixSystemKeys,
  });
  if (!tunes.length || !tunes.some(t => t.lines.some(l => l.staff?.length)))
    throw new Error("ABC contains no music to render.");
  const warnings = tunes.flatMap(t => t.warnings || []);
  if (warnings.length) throw new Error("Invalid ABC: " + warnings.join("; "));
  await document.fonts.ready;
  const ns = "http://www.w3.org/2000/svg";
  const pages = [];
  let page, content, used = 0, systems = 0;
  function addPage() {
    page = document.createElementNS(ns, "svg");
    for (const [k, v] of Object.entries({xmlns:ns, width:794, height:1123, viewBox:"0 0 794 1123"}))
      page.setAttribute(k,v);
    page.setAttribute("class", "score-page");
    const fontStyle = document.createElementNS(ns, "style");
    fontStyle.textContent = '@font-face{font-family:SheetSageSans;src:url(data:font/ttf;base64,' + window.renderFontData + ')}text{font-family:SheetSageSans !important}';
    page.append(fontStyle);
    const license = document.createElementNS(ns, "metadata");
    license.textContent = window.renderFontLicense;
    page.append(license);
    const background = document.createElementNS(ns, "rect");
    for (const [k,v] of Object.entries({width:794,height:1123,fill:"white"})) background.setAttribute(k,v);
    page.append(background);
    content = document.createElementNS(ns, "g");
    content.setAttribute("transform", "translate(40 40)");
    page.append(content);
    document.getElementById("pages").append(page);
    pages.push(page);
    used = 0;
  }
  for (const svg of source.querySelectorAll("svg")) {
    const box = svg.getBoundingClientRect();
    if (!(box.width > 0 && box.height > 0)) continue;
    const scale = Math.min(1, 714 / box.width);
    const height = box.height * scale;
    if (height > 1043) throw new Error("A staff system is taller than one page.");
    if (!page || used + height > 1043) addPage();
    const group = document.createElementNS(ns, "g");
    group.setAttribute("transform", `translate(0 ${used}) scale(${scale})`);
    const clone = svg.cloneNode(true);
    clone.setAttribute("width",box.width);
    clone.setAttribute("height",box.height);
    clone.style.overflow = "visible";
    group.append(clone);
    content.append(group);
    used += height;
    systems++;
  }
  source.remove();
  if (!pages.length) throw new Error("No score pages were produced.");
  return {pages:pages.map(p => new XMLSerializer().serializeToString(p)), systems, warnings};
};

window.renderAudio = async ({tracks, duration}) => {
  const context = new AudioContext({sampleRate:44100});
  await context.resume();
  const sequence = new ABCJS.synth.SynthSequence();
  for (const track of tracks) {
    const id = sequence.addTrack();
    sequence.setInstrument(id, 0);
    for (const note of track.notes) sequence.tracks[id].push({
      cmd:"note", instrument:0, pitch:note.pitch, volume:note.velocity,
      start:note.start, duration:note.end-note.start, gap:0,
    });
  }
  sequence.totalDuration = duration;
  const synth = new ABCJS.synth.CreateSynth();
  const loaded = await synth.init({audioContext:context, sequence, millisecondsPerMeasure:1000,
    options:{soundFontUrl:"https://render.invalid/soundfonts/", fadeLength:35,
      programOffsets:{acoustic_grand_piano:0}, soundFontVolumeMultiplier:1}});
  if (loaded.error?.length) throw new Error("Could not load piano samples: " + loaded.error.join(", "));
  await synth.prime();
  const buffer = synth.getAudioBuffer();
  if (!buffer) throw new Error("The renderer returned no audio.");
  // Simultaneous voices retain their balance; a peak guard avoids PCM clipping.
  let peak = 0;
  for (let c=0; c<buffer.numberOfChannels; c++) {
    const channel = buffer.getChannelData(c);
    for (let i=0; i<channel.length; i++) peak = Math.max(peak, Math.abs(channel[i]));
  }
  const gain = Math.min(0.7, peak > 0 ? 0.98 / peak : 0.7);
  window.renderedAudio = {buffer, gain};
  await context.close();
  return {frames:buffer.length, sample_rate:buffer.sampleRate, channels:buffer.numberOfChannels, gain};
};

window.audioBlock = ({start, count}) => {
  const {buffer, gain} = window.renderedAudio;
  const frames = Math.min(count, buffer.length-start);
  const bytes = new Uint8Array(frames*buffer.numberOfChannels*2);
  const view = new DataView(bytes.buffer);
  for (let c=0; c<buffer.numberOfChannels; c++) {
    const channel = buffer.getChannelData(c);
    for (let i=0; i<frames; i++) {
      const sample = Math.max(-1,Math.min(1,channel[start+i]*gain));
      view.setInt16((i*buffer.numberOfChannels+c)*2, Math.round(sample*32767), true);
    }
  }
  let binary="";
  for (let offset=0; offset<bytes.length; offset+=8192)
    binary += String.fromCharCode(...bytes.subarray(offset,offset+8192));
  return btoa(binary);
};