Compare commits

..

No commits in common. "bmpExtra" and "main" have entirely different histories.

23 changed files with 88874 additions and 527 deletions

1
README.md Normal file
View File

@ -0,0 +1 @@
# conductor

View File

@ -8,13 +8,7 @@
"bdrumnew-hit-v7-rr1-sum-(1).mp3" "bdrumnew-hit-v7-rr1-sum-(1).mp3"
], ],
"midi-channel-name": "conductorOrchestralBass", "midi-channel-name": "conductorOrchestralBass",
"image": { "image": { "filename": "bass.svg", "yPos": "13%" }
"filename": "bass.svg",
"yPos": "2.5%",
"width": "100",
"height": "100"
}
}, },
"Snare": { "Snare": {
"directory": "/assets/instruments/snare", "directory": "/assets/instruments/snare",
@ -23,34 +17,19 @@
"ropesnare-low-tsn-main-vl4-rr1.mp3" "ropesnare-low-tsn-main-vl4-rr1.mp3"
], ],
"midi-channel-name": "conductorSnare", "midi-channel-name": "conductorSnare",
"image": { "image": { "filename": "snare.svg", "yPos": "26%" }
"filename": "snare.svg",
"yPos": "7%",
"width": "100",
"height": "100"
}
}, },
"Surdo": { "Surdo": {
"directory": "/assets/instruments/surdo", "directory": "/assets/instruments/surdo",
"filenames": ["surdo-5.mp3", "surdo-6.mp3", "surdo-7.mp3"], "filenames": ["surdo-5.mp3", "surdo-6.mp3", "surdo-7.mp3"],
"midi-channel-name": "conductorSurdo", "midi-channel-name":"conductorSnare",
"image": { "image": { "filename": "surdo.svg", "yPos": "39%" }
"filename": "surdo.svg",
"yPos": "10.5%",
"width": "100",
"height": "100"
}
}, },
"Surdo Napa": { "Surdo Napa": {
"directory": "/assets/instruments/surdo-napa", "directory": "/assets/instruments/surdo-napa",
"filenames": ["surdo-1.mp3", "surdo-3.mp3", "surdo-4.mp3"], "filenames": ["surdo-1.mp3", "surdo-3.mp3", "surdo-4.mp3"],
"midi-channel-name": "conductorSurdoNapa", "midi-channel-name": "conductorSurdoNapa",
"image": { "image": { "filename": "surdo-napa.svg", "yPos": "42%" }
"filename": "surdo-napa.svg",
"yPos": "12%",
"width": "100",
"height": "100"
}
}, },
"Timpani Large": { "Timpani Large": {
"directory": "/assets/instruments/timpani-large", "directory": "/assets/instruments/timpani-large",
@ -61,27 +40,16 @@
"timpani7d-hit-v2-rr1-main.mp3" "timpani7d-hit-v2-rr1-main.mp3"
], ],
"midi-channel-name":"conductorTimpaniLarge", "midi-channel-name":"conductorTimpaniLarge",
"image": { "image": { "filename": "timpani-large.svg", "yPos": "55%" }
"filename": "timpani-large.svg",
"yPos": "14%",
"width": "100",
"height": "100"
}
}, },
"Timpani Small": { "Timpani Small": { "directory": "/assets/instruments/timpani-small",
"directory": "/assets/instruments/timpani-small",
"filenames": [ "filenames": [
"timpani1a-hit-v5-rr1-main.mp3", "timpani1a-hit-v5-rr1-main.mp3",
"timpani2-hit-v3-rr1-sum.mp3", "timpani2-hit-v3-rr1-sum.mp3",
"timpani4-hit-v2-rr1-sum.mp3" "timpani4-hit-v2-rr1-sum.mp3"
], ],
"midi-channel-name": "conductorTimpaniSmall", "midi-channel-name": "conductorTimpaniSmall",
"image": { "image": { "filename": "timpani-small.svg", "yPos": "68%" }
"filename": "timpani-small.svg",
"yPos": "18%",
"width": "100",
"height": "100"
}
}, },
"Toms": { "Toms": {
"directory": "/assets/instruments/toms", "directory": "/assets/instruments/toms",
@ -91,11 +59,6 @@
"toml-rollm-v2-rr1-mid.mp3" "toml-rollm-v2-rr1-mid.mp3"
], ],
"midi-channel-name": "conductorToms", "midi-channel-name": "conductorToms",
"image": { "image": { "filename": "toms.svg", "yPos": "81%" }
"filename": "toms.svg",
"yPos": "42%",
"width": "100",
"height": "100"
}
} }
} }

View File

@ -1 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23.33 18.79" width="200" height="200"><defs><style>.cls-1{fill:#fff;stroke-width:0px;}.cls-2{fill:none;stroke:#fff;stroke-miterlimit:10;}</style></defs><g id="Layer_1-2"><path class="cls-1" d="M19.09,7.44c0,4.11-3.32,7.44-7.42,7.44s-7.42-3.33-7.42-7.44S7.56,0,11.66,0s7.42,3.33,7.42,7.44"/><path class="cls-2" d="M22.83,9.31c0,4.96-5,8.98-11.16,8.98S.5,14.27.5,9.31"/></g></svg> <?xml version="1.0" encoding="UTF-8"?><svg id="Layer_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23.33 18.79"><defs><style>.cls-1{fill:#fff;stroke-width:0px;}.cls-2{fill:none;stroke:#fff;stroke-miterlimit:10;}</style></defs><g id="Layer_1-2"><path class="cls-1" d="M19.09,7.44c0,4.11-3.32,7.44-7.42,7.44s-7.42-3.33-7.42-7.44S7.56,0,11.66,0s7.42,3.33,7.42,7.44"/><path class="cls-2" d="M22.83,9.31c0,4.96-5,8.98-11.16,8.98S.5,14.27.5,9.31"/></g></svg>

Before

Width:  |  Height:  |  Size: 482 B

After

Width:  |  Height:  |  Size: 457 B

View File

@ -1 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15.27 23.95" width="100" height="100"><defs><style>.cls-1{fill:#fff;stroke-width:0px;}.cls-2{fill:none;stroke:#fff;stroke-miterlimit:10;}</style></defs><g id="Layer_1-2"><polyline class="cls-2" points="14.97 23.55 .78 12.61 10.65 3.86"/><path class="cls-1" d="M14.98,0c-1.35,2.55-2.72,6.05-3.05,8.78l-1.84-4.44-4.31-2.26c2.8-.05,6.49-1.02,9.2-2.08"/><line class="cls-2" x1=".76" y1="23.55" x2="7.02" y2="17.42"/></g></svg> <?xml version="1.0" encoding="UTF-8"?><svg id="Layer_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15.27 23.95"><defs><style>.cls-1{fill:#fff;stroke-width:0px;}.cls-2{fill:none;stroke:#fff;stroke-miterlimit:10;}</style></defs><g id="Layer_1-2"><polyline class="cls-2" points="14.97 23.55 .78 12.61 10.65 3.86"/><path class="cls-1" d="M14.98,0c-1.35,2.55-2.72,6.05-3.05,8.78l-1.84-4.44-4.31-2.26c2.8-.05,6.49-1.02,9.2-2.08"/><line class="cls-2" x1=".76" y1="23.55" x2="7.02" y2="17.42"/></g></svg>

Before

Width:  |  Height:  |  Size: 526 B

After

Width:  |  Height:  |  Size: 501 B

View File

@ -1 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 13.43 23.52" width="100" height="100"><defs><style>.cls-1{fill:none;stroke:#fff;stroke-miterlimit:10;stroke-width:.87px;}</style></defs><g id="Layer_1-2"><polygon class="cls-1" points="6.71 22.55 .71 10.73 12.72 10.73 6.71 22.55"/><path class="cls-1" d="M1.03,4.12h11.36M2.7.32l8.03,7.6M10.73.32L2.7,7.92"/></g></svg> <?xml version="1.0" encoding="UTF-8"?><svg id="Layer_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 13.43 23.52"><defs><style>.cls-1{fill:none;stroke:#fff;stroke-miterlimit:10;stroke-width:.87px;}</style></defs><g id="Layer_1-2"><polygon class="cls-1" points="6.71 22.55 .71 10.73 12.72 10.73 6.71 22.55"/><path class="cls-1" d="M1.03,4.12h11.36M2.7.32l8.03,7.6M10.73.32L2.7,7.92"/></g></svg>

Before

Width:  |  Height:  |  Size: 422 B

After

Width:  |  Height:  |  Size: 396 B

View File

@ -1 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 13.43 23.67" width="100" height="100"><defs><style>.cls-1{fill:#fff;stroke-width:0px;}.cls-2{fill:none;stroke:#fff;stroke-miterlimit:10;stroke-width:.87px;}</style></defs><g id="Layer_1-2"><polygon class="cls-2" points="6.71 22.71 .71 10.89 12.72 10.89 6.71 22.71"/><path class="cls-1" d="M11.33,4.54c0,2.51-2.07,4.54-4.62,4.54S2.09,7.05,2.09,4.54,4.16,0,6.71,0s4.62,2.03,4.62,4.54"/></g></svg> <?xml version="1.0" encoding="UTF-8"?><svg id="Layer_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 13.43 23.67"><defs><style>.cls-1{fill:#fff;stroke-width:0px;}.cls-2{fill:none;stroke:#fff;stroke-miterlimit:10;stroke-width:.87px;}</style></defs><g id="Layer_1-2"><polygon class="cls-2" points="6.71 22.71 .71 10.89 12.72 10.89 6.71 22.71"/><path class="cls-1" d="M11.33,4.54c0,2.51-2.07,4.54-4.62,4.54S2.09,7.05,2.09,4.54,4.16,0,6.71,0s4.62,2.03,4.62,4.54"/></g></svg>

Before

Width:  |  Height:  |  Size: 498 B

After

Width:  |  Height:  |  Size: 473 B

View File

@ -1 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 21.29 23.81" width="100" height="100"><defs><style>.cls-1{fill:#fff;stroke-width:0px;}.cls-2{fill:none;stroke:#fff;stroke-miterlimit:10;}</style></defs><g id="Layer_1-2"><polyline class="cls-2" points="0 .5 19.76 .5 5.02 11.32 15.28 19.98"/><path class="cls-1" d="M19.84,23.81c-2.83-1.05-6.66-2.01-9.57-2.06l4.48-2.23,1.92-4.4c.35,2.7,1.76,6.17,3.18,8.69"/><line class="cls-2" x1="1.74" y1="23.81" x2="1.74" y2=".47"/></g></svg> <?xml version="1.0" encoding="UTF-8"?><svg id="Layer_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 21.29 23.81"><defs><style>.cls-1{fill:#fff;stroke-width:0px;}.cls-2{fill:none;stroke:#fff;stroke-miterlimit:10;}</style></defs><g id="Layer_1-2"><polyline class="cls-2" points="0 .5 19.76 .5 5.02 11.32 15.28 19.98"/><path class="cls-1" d="M19.84,23.81c-2.83-1.05-6.66-2.01-9.57-2.06l4.48-2.23,1.92-4.4c.35,2.7,1.76,6.17,3.18,8.69"/><line class="cls-2" x1="1.74" y1="23.81" x2="1.74" y2=".47"/></g></svg>

Before

Width:  |  Height:  |  Size: 533 B

After

Width:  |  Height:  |  Size: 507 B

View File

@ -1 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15.32 23.67" width="100" height="100"><defs><style>.cls-1{fill:#fff;stroke-width:0px;}.cls-2{fill:none;stroke:#fff;stroke-miterlimit:10;}</style></defs><g id="Layer_1-2"><polyline class="cls-2" points="15.02 .4 .79 11.21 10.3 19.39"/><path class="cls-1" d="M15.22,23.67c-2.73-1.05-6.43-2.01-9.24-2.06l4.33-2.23,1.85-4.39c.34,2.7,1.7,6.16,3.07,8.68"/></g></svg> <?xml version="1.0" encoding="UTF-8"?><svg id="Layer_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15.32 23.67"><defs><style>.cls-1{fill:#fff;stroke-width:0px;}.cls-2{fill:none;stroke:#fff;stroke-miterlimit:10;}</style></defs><g id="Layer_1-2"><polyline class="cls-2" points="15.02 .4 .79 11.21 10.3 19.39"/><path class="cls-1" d="M15.22,23.67c-2.73-1.05-6.43-2.01-9.24-2.06l4.33-2.23,1.85-4.39c.34,2.7,1.7,6.16,3.07,8.68"/></g></svg>

Before

Width:  |  Height:  |  Size: 465 B

After

Width:  |  Height:  |  Size: 439 B

View File

@ -1 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 13.43 23.96" width="100" height="100"><defs><style>.cls-1{fill:#fff;stroke-width:0px;}.cls-2{stroke-miterlimit:10;}.cls-2,.cls-3{fill:none;stroke:#fff;stroke-width:.87px;}</style></defs><g id="Layer_1-2"><polygon class="cls-2" points="6.71 22.99 .71 11.18 12.72 11.18 6.71 22.99"/><path class="cls-1" d="M9.11,7.86c0,1.3-1.07,2.36-2.4,2.36s-2.4-1.05-2.4-2.36,1.07-2.36,2.4-2.36,2.4,1.06,2.4,2.36"/><path class="cls-1" d="M9.11,2.36c0,1.3-1.07,2.36-2.4,2.36s-2.4-1.06-2.4-2.36S5.39,0,6.71,0s2.4,1.06,2.4,2.36"/><line class="cls-3" x1="3.46" y1="16.34" x2="9.97" y2="16.34"/></g></svg> <?xml version="1.0" encoding="UTF-8"?><svg id="Layer_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 13.43 23.96"><defs><style>.cls-1{fill:#fff;stroke-width:0px;}.cls-2{stroke-miterlimit:10;}.cls-2,.cls-3{fill:none;stroke:#fff;stroke-width:.87px;}</style></defs><g id="Layer_1-2"><polygon class="cls-2" points="6.71 22.99 .71 11.18 12.72 11.18 6.71 22.99"/><path class="cls-1" d="M9.11,7.86c0,1.3-1.07,2.36-2.4,2.36s-2.4-1.05-2.4-2.36,1.07-2.36,2.4-2.36,2.4,1.06,2.4,2.36"/><path class="cls-1" d="M9.11,2.36c0,1.3-1.07,2.36-2.4,2.36s-2.4-1.06-2.4-2.36S5.39,0,6.71,0s2.4,1.06,2.4,2.36"/><line class="cls-3" x1="3.46" y1="16.34" x2="9.97" y2="16.34"/></g></svg>

Before

Width:  |  Height:  |  Size: 688 B

After

Width:  |  Height:  |  Size: 662 B

File diff suppressed because one or more lines are too long

View File

@ -15,6 +15,11 @@
<div class="loading"> <div class="loading">
</div> </div>
</div> </div>
<div id="buttons">
<img src="/assets/svg/mute.svg" class="audio" id="mute" />
<img src="/assets/svg/menu.svg" class="menu" id="menu-button" />
</div>
<div id="main-content"> <div id="main-content">
@ -33,6 +38,56 @@
id="time-indicator"></object> id="time-indicator"></object>
</div> </div>
</div> </div>
<div id="about-content">
<div class="about-title">
<img src="/assets/svg/the-conductor-title.svg"></img>
</div>
<div class="about-content">
<div class="safari">
<div class="instruments-key" id="instrument-key-div"></div>
</div>
<p>
The Conductor translates live lightning strikes from around the world into a graphic score for seven percussion
instruments.
</p>
<p>
The project was conceived and developed by the artist Mishka Henner between 2023 and 2024 with a team of
musicians, sound artists and acoustics engineers during an artist's residency at Energy House 2.0 at the
University of Salford.
</p>
<p>
The Conductor was first performed at the University of Salfords Acoustic Laboratories during the Sounds From
the Other City Festival on 5 May 2024.
</p>
<p>
Click <a href="https://mishkahenner.com/The-Conductor" style="color: #fff; text-decoration:underline">here</a>
to read more about the project. Download a copy of the artist's graphic score <a
href="https://files.cargocollective.com/c20096/CONDUCTOR-SHEET-MUSIC-Black-BG.pdf"
style="color: #fff; text-decoration:underline">here</a>.
</p>
<br />
<div class="production-credits">
<br />
<div>
Project Conception and Design by Mishka Henner.
</div>
Data Capture, Web Design and Coding by Joe Gibson.
<div>
Lightning data from Blitzortung.org
</div>
<p>
With support from the University of Salford Art Collection in partnership with Open Eye Gallery, Liverpool as
part of the LOOK Photo Biennial, and Castlefield Gallery, Manchester. Generously supported by Friends of
Energy House Labs.
</p>
</div>
</div>
</div>
<script type="module" src="/src/script.js"></script> <script type="module" src="/src/script.js"></script>
</body> </body>

18
notes.txt Normal file
View File

@ -0,0 +1,18 @@
stop zooming issue - resize rules
mishka gonna send an x icon for the back behaviour
look at memory leak??
make the sound and menu icons part of an 'app bar' along with the conductor logo
make the mid size the default size but double check the size on the phone isnt too small
refactor when it's all done
format the key as per the pdf
sort out the title size
sort out the blitzortung connection issue
make the text max size smaller
about page make title smaller

View File

@ -1,45 +0,0 @@
let audioContext;
let gainNode;
function startAudio() {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
gainNode = audioContext.createGain();
gainNode.gain.value = 0;
gainNode.connect(audioContext.destination);
}
function muter() {
fadeOutVolume();
}
function unMute() {
startAudio();
fadeInVolume();
}
function fadeInVolume() {
gainNode.gain.setValueAtTime(0, audioContext.currentTime); // Start at 0
gainNode.gain.linearRampToValueAtTime(1, audioContext.currentTime + 2); // Fade to full volume
}
function fadeOutVolume() {
gainNode.gain.setValueAtTime(1, audioContext.currentTime); // Start at 1
gainNode.gain.linearRampToValueAtTime(0, audioContext.currentTime + 2); // Fade to 0
}
export function playSound(instrument) {
if (interacted) {
// const source = audioContext.createBufferSource();
const samples = instrument.samples;
const sampleKeys = Object.keys(samples);
const numSamples = sampleKeys.length;
const randomSampleNumber = Math.floor(Math.random() * (numSamples - 0));
// const randomKey =
// sampleKeys[randomSampleNumber];
sendMidiMessage(instrument.midiChannelName, randomSampleNumber);
// source.buffer = samples[`${randomKey}`];
// source.connect(gainNode);
// source.start();
}
}

View File

@ -1,62 +0,0 @@
import { loadInstruments } from "./instruments.js";
export class Conductor {
instruments;
constructor() {}
showingWhichContent;
interacted = true;
get instruments() {
return this.instruments;
}
static setTitle() {
const now = new Date();
const future = new Date(now.getTime() + 59 * 1000); // 59 seconds later
const formattedDate = `${Conductor.padZero(
now.getDate()
)}.${Conductor.padZero(now.getMonth() + 1)}.${now.getFullYear()}`;
const formattedTime = `${Conductor.padZero(
now.getHours()
)}:${Conductor.padZero(now.getMinutes())}:${this.padZero(
now.getSeconds()
)}`;
const formattedFutureTime = `${Conductor.padZero(
future.getHours()
)}:${Conductor.padZero(future.getMinutes())}:${Conductor.padZero(
future.getSeconds()
)}`;
title.innerHTML = `${formattedDate}<span class="tab-space"></span>${formattedTime} - ${formattedFutureTime} UTC`;
}
showMainContent() {
Conductor.setTitle();
this.fadeAndReplaceADiv("main-content", "loading-screen");
}
fadeAndReplaceADiv(innyDivId, outtyDivId) {
const outty = document.getElementById(outtyDivId);
const inny = document.getElementById(innyDivId);
outty.classList.remove("fade-in");
outty.classList.add("fade-out");
outty.style.display = "none";
inny.style.display = "flex";
inny.style.opacity = "0";
void inny.offsetWidth;
inny.classList.add("fade-in");
inny.style.opacity = "1";
}
static padZero(num) {
return num.toString().padStart(2, "0");
}
async init() {
this.instruments = await loadInstruments();
}
}

View File

@ -3,67 +3,62 @@ async function loadInstrumentsConfig() {
return await response.json(); return await response.json();
} }
async function fetchSvgText(url) { async function fetchAudioData(url) {
const response = await fetch(url); const response = await fetch(url);
return await response.text(); const arrayBuffer = await response.arrayBuffer();
return arrayBuffer;
} }
function svgTextToImage(svgText, defaultWidth = 100, defaultHeight = 100) { async function decodeAudioData(context, arrayBuffer) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// Create a blob from the SVG text context.decodeAudioData(arrayBuffer, resolve, reject);
const blob = new Blob([svgText], { type: "image/svg+xml" }); });
const url = URL.createObjectURL(blob);
const img = new Image();
// Set the onload callback
img.onload = () => {
// Check if naturalWidth and naturalHeight are invalid
if (img.naturalWidth === 0 || img.naturalHeight === 0) {
// Set default dimensions if image is invalid (missing width/height in SVG)
img.width = defaultWidth;
img.height = defaultHeight;
} }
// Clean up the object URL to free memory async function loadAudioSamples(context, directory, filenames) {
URL.revokeObjectURL(url); const audioFiles = filenames;
resolve(img); const audioBuffers = {};
}; for (const file of audioFiles) {
const arrayBuffer = await fetchAudioData(`${directory}/${file}`);
const audioBuffer = await decodeAudioData(context, arrayBuffer);
const fileName = file.split("/").pop(); // Extract filename without path for reference
audioBuffers[fileName] = audioBuffer;
}
// Handle image loading errors return audioBuffers;
img.onerror = (err) => { }
URL.revokeObjectURL(url); // Clean up URL
reject(err); async function fetchImageData(url) {
}; const response = await fetch(url);
img.src = url; const blob = await response.blob();
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(blob); // Convert to base64
}); });
} }
export async function loadInstruments() { export async function loadInstruments() {
const instrumentsConfig = await loadInstrumentsConfig(); const instrumentsConfig = await loadInstrumentsConfig();
const context = new (window.AudioContext || window.webkitAudioContext)();
const instruments = {}; const instruments = {};
for (const [instrument, config] of Object.entries(instrumentsConfig)) { for (const [instrument, config] of Object.entries(instrumentsConfig)) {
const numSamples = config.filenames.length; const samples = await loadAudioSamples(
context,
const svgText = await fetchSvgText( config.directory,
config.filenames
);
const imageData = await fetchImageData(
`${config.directory}/${config.image.filename}` `${config.directory}/${config.image.filename}`
); );
// Pass the width and height to the svgTextToImage function
const img = await svgTextToImage(
svgText,
config.image.width || 100,
config.image.height || 100
);
instruments[instrument] = { instruments[instrument] = {
numSamples: numSamples, samples: samples,
image: img, image: imageData,
yPos: config.image.yPos, yPos: config.image.yPos, // Store base64-encoded image data
midiChannelName: config["midi-channel-name"], midiChannelName: config["midi-channel-name"],
width: config.image.width,
height: config.image.height,
}; };
} }

View File

@ -1,6 +1,6 @@
export async function sendMidiMessage(midiChannelName, sampleNumber, midiAccess) { export async function sendMidiMessage(midiChannelName, sampleNumber ) {
try { try {
console.log(midiChannelName, sampleNumber); const midiAccess = await navigator.requestMIDIAccess();
const availableMidiOutputs = midiAccess.outputs.values(); const availableMidiOutputs = midiAccess.outputs.values();
let midiOut; let midiOut;
@ -17,12 +17,13 @@ export async function sendMidiMessage(midiChannelName, sampleNumber, midiAccess)
} }
// MIDI Note On (Play Note) // MIDI Note On (Play Note)
midiOut.send([0x90, 35 + sampleNumber, 127]); midiOut.send([0x90, 36 + sampleNumber, 127]);
// MIDI Note Off after 500ms // MIDI Note Off after 500ms
setTimeout(() => { setTimeout(() => {
midiOut.send([0x80, 35 + sampleNumber, 0]); // Stop Note midiOut.send([0x80, 36 + sampleNumber, 0]); // Stop Note
}, 500); }, 500);
} catch (error) { } catch (error) {
console.error("Failed to get MIDI access:", error); console.error("Failed to get MIDI access:", error);
} }

View File

@ -1,135 +0,0 @@
import { Conductor } from "./conductor.js";
export class Renderer {
timeIndicator;
currStaveNumber;
numStaves;
sheetWindow;
canvases = [];
iconCache = [];
iconScale = 0.20;
constructor(dataStream, strikeHandler) {
this.dataStream = dataStream;
this.strikeHandler = strikeHandler;
this.numStaves = 6;
this.currStaveNumber = 1;
this.timeIndicator = document.getElementById("time-indicator");
this.sheetWindow = document.getElementById("music-window");
window.addEventListener("resize", () => {
this.redrawIcons();
});
this.timeIndicator.addEventListener("animationiteration", () => {
this.currStaveNumber++;
if (this.currStaveNumber > this.numStaves) {
this.dataStream.removeEventListener("strike", this.strikeHandler);
setTimeout( () =>{this.dataStream.addEventListener("strike", this.strikeHandler);
this.cleanUpAndRestart();}, 60000);
}
});
for (let i = 1; i <= this.numStaves; i++) {
const staveWrapper = document.createElement("div");
staveWrapper.classList.add("stave-wrapper");
staveWrapper.setAttribute("id", `stave-wrapper-${i}`);
staveWrapper.style.position = "relative";
const staveObject = document.createElement("object");
staveObject.type = "image/svg+xml";
staveObject.data = "assets/svg/stave.svg";
staveObject.className = "stave-svg"; // Fixed className
const canvas = document.createElement("canvas");
canvas.id = `canvas-${i}`;
canvas.className = "event-canvas";
const ctx = canvas.getContext("2d");
this.canvases.push({ canvas, ctx });
staveWrapper.appendChild(staveObject);
staveWrapper.appendChild(canvas);
this.sheetWindow.appendChild(staveWrapper);
// Your exact sizing method + minimal DPR adjustment
requestAnimationFrame(() => {
const dpr = window.devicePixelRatio || 1;
// Keep your offset measurements
const displayWidth = canvas.offsetWidth;
const displayHeight = canvas.offsetHeight;
// Apply to actual canvas buffer
canvas.width = Math.round(displayWidth * dpr);
canvas.height = Math.round(displayHeight * dpr);
// Maintain your display size
canvas.style.width = `${displayWidth}px`;
canvas.style.height = `${displayHeight}px`;
// Scale context to compensate
ctx.scale(dpr , dpr);
});
}
}
async placeIcon(instrument) {
if (this.currStaveNumber > this.numStaves) {
this.currStaveNumber = 1;
this.iconCache.length = 0;
}
const rect = this.timeIndicator.getBoundingClientRect();
const sheetLeft = this.sheetWindow.getBoundingClientRect().left;
const xPosition = rect.left + window.scrollX - sheetLeft;
const canvasEntry = this.canvases[this.currStaveNumber - 1];
const canvas = canvasEntry.canvas;
const ctx = canvasEntry.ctx;
const yPercent = parseFloat(instrument.yPos);
const y = (yPercent / 100) * canvas.height;
const width = instrument.width * this.iconScale;
const height = instrument.height * this.iconScale;
ctx.drawImage(instrument.image, xPosition, y, width, height);
this.iconCache.push({
image: instrument.image,
x: xPosition,
y,
width,
height,
stave: this.currStaveNumber,
});
}
redrawIcons() {
this.canvases.forEach(({ ctx, canvas }) => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
});
this.iconCache.forEach(({ image, x, y, width, height, stave }) => {
const ctx = this.canvases[stave - 1].ctx;
ctx.drawImage(image, x, y, width, height);
});
}
cleanUpAndRestart(reload = false) {
this.iconCache = [];
this.currStaveNumber = 1;
this.canvases.forEach(({ ctx, canvas }) => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
});
Conductor.setTitle();
if (reload) location.reload();
}
}

View File

@ -1,44 +1,294 @@
import { Client, BrowserSocketFactory } from "./blitz.js";
import { loadInstruments } from "./instruments.js";
import { sendMidiMessage } from "./midi.js"; import { sendMidiMessage } from "./midi.js";
import { Conductor } from "./conductor.js";
import { DataStream } from "./socket.js";
import { Renderer } from "./render.js";
document.addEventListener("DOMContentLoaded", async () => { document.addEventListener("DOMContentLoaded", async () => {
let showing;
let audioContext;
let interacted = true;
let gainNode;
const midiAccess = await navigator.requestMIDIAccess(); sendMidiMessage();
const dataStream = new DataStream();
const conductor = new Conductor();
await conductor.init();
const instruments = await conductor.instruments;
let strikes = 0;
const strikeEvery = 2;
const strikeHandler = () => { function startAudio() {
console.log(strikes); audioContext = new (window.AudioContext || window.webkitAudioContext)();
//** select instrument */ gainNode = audioContext.createGain();
if (strikes === strikeEvery){ gainNode.gain.value = 0;
gainNode.connect(audioContext.destination);
const keys = Object.keys(instruments);
const randomIndex = Math.floor(Math.random() * keys.length);
const selectedInstrument = instruments[keys[randomIndex]];
//** select sample */
const numSamples = selectedInstrument.numSamples;
const randomSampleNumber = Math.floor(Math.random() * numSamples);
renderer.placeIcon(selectedInstrument);
sendMidiMessage(selectedInstrument.midiChannelName, randomSampleNumber + 1, midiAccess);
strikes = 0;
} }
strikes++;
function muter() {
fadeOutVolume();
}
function unMute() {
startAudio();
fadeInVolume();
}
function padZero(num) {
return num.toString().padStart(2, "0");
}
function setTitle() {
const now = new Date();
const future = new Date(now.getTime() + 59 * 1000); // 59 seconds later
const formattedDate = `${padZero(now.getDate())}.${padZero(
now.getMonth() + 1
)}.${now.getFullYear()}`;
const formattedTime = `${padZero(now.getHours())}:${padZero(
now.getMinutes()
)}:${padZero(now.getSeconds())}`;
const formattedFutureTime = `${padZero(future.getHours())}:${padZero(
future.getMinutes()
)}:${padZero(future.getSeconds())}`;
title.innerHTML = `${formattedDate}<span class="tab-space"></span>${formattedTime} - ${formattedFutureTime} UTC`;
}
function fadeInVolume() {
gainNode.gain.setValueAtTime(0, audioContext.currentTime); // Start at 0
gainNode.gain.linearRampToValueAtTime(1, audioContext.currentTime + 2); // Fade to full volume
}
function fadeOutVolume() {
gainNode.gain.setValueAtTime(1, audioContext.currentTime); // Start at 1
gainNode.gain.linearRampToValueAtTime(0, audioContext.currentTime + 2); // Fade to 0
}
function showMainContent() {
setTitle();
const mute = document.getElementById("mute");
mute.addEventListener("click", () => {
const mutedSrc = "/assets/svg/sound-on.svg";
const unMutedSrc = "/assets/svg/mute.svg";
interacted = !interacted;
interacted ? unMute() : muter();
mute.src = interacted ? mutedSrc : unMutedSrc;
startAudio();
fadeInVolume();
});
fadeAndReplaceADiv("main-content", "loading-screen");
fadeAndReplaceADiv("buttons", "buttons");
showing = "main-content";
document.getElementById("menu-button").onclick = function () {
toggleInfo();
};
}
function toggleInfo() {
if (showing === "main-content") {
fadeAndReplaceADiv("about-content", "main-content");
cleanUpAndRestart();
showing = "about-content";
} else {
fadeAndReplaceADiv("main-content", "about-content");
showing = "main-content";
}
}
function fadeAndReplaceADiv(innyDivId, outtyDivId) {
const outty = document.getElementById(outtyDivId);
const inny = document.getElementById(innyDivId);
outty.classList.remove("fade-in");
outty.classList.add("fade-out");
outty.style.display = "none";
inny.style.display = "flex";
inny.style.opacity = "0";
void inny.offsetWidth;
inny.classList.add("fade-in");
inny.style.opacity = "1";
}
const socketFactory = new BrowserSocketFactory();
let client = new Client(socketFactory);
client.connect();
let connectionTimeout; // Timeout for detecting inactivity
let lastReceived = Date.now(); // Tracks the last time data was received
function resetTimeout() {
clearTimeout(connectionTimeout);
connectionTimeout = setTimeout(() => {
// 15 seconds of inactivity
console.log("No data received in 15 seconds, reconnecting...");
reconnectWebSocket();
}, 15000);
}
function reconnectWebSocket() {
console.log("Reconnecting WebSocket...");
// Clean up the existing WebSocket connection
if (client) {
client.close(); // Ensure the current connection is closed
}
// Create a new client instance and re-establish the connection
client = new Client(new BrowserSocketFactory());
setupWebSocketListeners(); // Set up listeners for the new WebSocket
client.connect(); // Reconnect
// Reset timeout after reconnecting
resetTimeout(); // Ensure timeout is re-established after reconnection
}
function setupWebSocketListeners() {
client.removeAllListeners("data");
client.removeAllListeners("error");
client.removeAllListeners("close");
client.on("data", (strike) => {
lastReceived = Date.now(); // Update the last received timestamp
resetTimeout(); // Reset the timeout on any data received
// Process the strike (existing logic from your original code)
strikeNumber++;
if (strikeNumber == 1) {
const randomInstrumentNumber = Math.floor(
Math.random() * (Object.keys(instruments).length - 1)
);
const selectedInstrumentName =
Object.keys(instruments)[randomInstrumentNumber];
placeIcon(instruments[selectedInstrumentName]);
playSound(instruments[selectedInstrumentName]);
strikeNumber = 0;
}
});
client.on("error", (message) => {
console.log("WebSocket error:", message);
reconnectWebSocket();
});
client.on("close", () => {
console.log("WebSocket closed, attempting to reconnect...");
reconnectWebSocket();
});
}
let currStaveNumber = 1;
const instruments = await loadInstruments();
const instNames = Object.keys(instruments);
showMainContent();
/***
* Add the instrument images and names into the about section
*/
const instrumentsKey = document.getElementById("instrument-key-div");
for (const instrument of instNames) {
const keyDiv = document.createElement("div");
const keySvg = document.createElement("object");
const keyName = document.createElement("div");
keyName.innerText = instrument.toUpperCase();
keySvg.type = "image/svg+xml";
keySvg.data = instruments[instrument].image;
keyDiv.appendChild(keySvg);
keyDiv.appendChild(keyName);
keyDiv.classList.add("instrument-key");
instrumentsKey.appendChild(keyDiv);
}
const numStaves = 6;
const sheetWindow = document.getElementById("music-window");
for (let i = 1; i <= numStaves; i++) {
const staveWrapper = document.createElement("div");
staveWrapper.classList.add(`stave-wrapper`);
staveWrapper.setAttribute("id", `stave-wrapper-${i}`);
staveWrapper.style.position = "relative";
const staveObject = document.createElement("object");
staveObject.type = "image/svg+xml";
staveObject.data = "assets/svg/stave.svg";
staveObject.classList.add("stave-svg");
staveWrapper.appendChild(staveObject);
sheetWindow.appendChild(staveWrapper);
}
const timeIndicator = document.querySelector("#time-indicator");
timeIndicator.addEventListener("animationiteration", () => {
currStaveNumber++;
if (currStaveNumber > numStaves) {
cleanUpAndRestart();
}
});
const cleanUpAndRestart = (reload = false) => {
const icons = document.querySelectorAll(".event-icon");
const timeIndicator = document.getElementById("time-indicator");
icons.forEach((icon) => {
icon.classList.add("fade-out");
icon.addEventListener("transitionend", () => {
icon.remove();
});
});
currStaveNumber = 1;
setTitle();
if (reload) location.reload();
}; };
const renderer = new Renderer(dataStream, strikeHandler); const playSound = (instrument) => {
if (interacted) {
// const source = audioContext.createBufferSource();
const samples = instrument.samples;
const sampleKeys = Object.keys(samples);
const numSamples = sampleKeys.length;
const randomSampleNumber = Math.floor(Math.random() * (numSamples - 1));
// const randomKey =
// sampleKeys[randomSampleNumber];
sendMidiMessage(instrument.midiChannelName, randomSampleNumber);
dataStream.addEventListener("strike", strikeHandler); // source.buffer = samples[`${randomKey}`];
// source.connect(gainNode);
// source.start();
}
};
dataStream.init(); const placeIcon = (instrument) => {
if (currStaveNumber > numStaves) currStaveNumber = 1;
// Select the staveWrapper in which to place the div
const staveWrapper = document.getElementById(
`stave-wrapper-${currStaveNumber}`
);
// Determine the position of the time indicator
const rect = timeIndicator.getBoundingClientRect();
const sheetLeft = sheetWindow.getBoundingClientRect().left;
const xPosition = rect.left + window.scrollX - sheetLeft;
conductor.showMainContent(); // Create the icon div and give it its location data
const newObject = document.createElement("div");
newObject.classList.add("event-icon");
newObject.style.position = "absolute";
newObject.style.left = `${
xPosition - document.documentElement.clientWidth / 200
}px`;
newObject.style.top = `${instrument.yPos}`;
// Create the icon
const eventIcon = document.createElement("object");
eventIcon.type = "image/svg+xml";
eventIcon.data = instrument.image;
// Append icon to div
newObject.appendChild(eventIcon);
// Append the div to the staveWrapper
staveWrapper.appendChild(newObject);
};
let strikeNumber = 0;
setupWebSocketListeners(); // Setup the WebSocket listeners
resetTimeout(); // Start the timeout timer
}); });

View File

@ -1,71 +0,0 @@
import { BrowserSocketFactory, Client } from "./blitz.js";
export class DataStream extends EventTarget {
constructor() {
super();
this.socketFactory = null;
this.client = null;
this.lastReceived = null;
this.connectionTimeout = null;
this.test = false;
this.testInterval = null;
}
async init() {
this.socketFactory = new BrowserSocketFactory();
this.client = new Client(this.socketFactory);
this.lastReceived = Date.now();
this.setupWebSocketListeners();
this.client.connect();
this.resetTimeout();
}
resetTimeout() {
clearTimeout(this.connectionTimeout);
this.connectionTimeout = setTimeout(() => {
console.log("No data received in 14 seconds, reconnecting...");
this.reconnectWebSocket();
}, 14999);
}
reconnectWebSocket() {
if (this.client) {
this.client.close();
}
this.client = new Client(new BrowserSocketFactory());
this.setupWebSocketListeners();
this.client.connect();
this.resetTimeout();
}
setupWebSocketListeners() {
if (this.test === false) {
this.client.removeAllListeners?.("data");
this.client.removeAllListeners?.("error");
this.client.removeAllListeners?.("close");
this.client.on("data", (data) => {
this.lastReceived = Date.now();
this.resetTimeout();
this.dispatchEvent(new CustomEvent("strike", { detail: data }));
});
this.client.on("error", (err) => {
console.error("WebSocket error:", err);
this.reconnectWebSocket();
});
this.client.on("close", () => {
console.log("WebSocket closed, attempting to reconnect...");
this.reconnectWebSocket();
});
} else {
clearInterval(this.testInterval);
this.testInterval = setInterval(() => {
this.dispatchEvent(new CustomEvent("strike", { detail: "fakeData" }));
}, 1000);
}
}
}

View File

@ -1,20 +1,20 @@
body { body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
background-color: rgb(0,0,0); background-color: rgb(24, 24, 24);
height: 100vh; height: 100vh;
overflow: hidden; overflow: hidden;
} }
object { object {
margin-bottom: 10px; margin-bottom: 10px;
} }
html { html {
overflow-x: visible; overflow-x: hidden;
} }
* { * {
@ -32,7 +32,7 @@ html {
align-items: center; align-items: center;
opacity: 0; opacity: 0;
transition: opacity 2s ease; transition: opacity 2s ease;
overflow: visible; overflow: hidden;
} }
@ -74,29 +74,6 @@ html {
transition: opacity 1s ease; transition: opacity 1s ease;
} }
.event-canvas {
position: absolute;
top: 0;
left: -10px;
height: 12vh;
width: 102%; /* Matches JS padding */
display: block;
pointer-events: none;
overflow: visible;
z-index: 9999;
}
.stave-wrapper {
position: relative;
display: block;
width: 100%;
height: 12vh;
background-color: rgb(0,0,0);
padding-top: 30px;
padding-bottom: 20px;
overflow: visible;
z-index: 9995;
}
.logo { .logo {
color: #fff; color: #fff;
@ -111,6 +88,10 @@ html {
margin: 20px; margin: 20px;
} }
.event-icon {
width: 1vw;
min-width: 10px;
}
.instruments-key { .instruments-key {
position: relative; position: relative;
@ -140,9 +121,8 @@ html {
max-width: 700px; max-width: 700px;
height: 70%; height: 70%;
margin: 0; margin: 0;
background-color: rgb(0,0,0); background-color: rgb(24,24,24);
margin-bottom: 100px; margin-bottom: 100px;
overflow: visible;
} }
.conductor-title{ .conductor-title{
@ -169,30 +149,27 @@ html {
z-index: 10; z-index: 10;
} }
.title{
top: 30px;
width: 100%;
margin: 30px;
text-align: center;
color: white;
background-color: rgb(24,24,24);
/* height: 20vh; */
font-family: Helvetica;
font-size: clamp(8px,1.1vw, 18px);
}
.about-title { .about-title {
top: 30px; top: 30px;
width: 50%; width: 50%;
} }
.title{
top: 30px;
width: 100%;
margin-top: 30px;
margin-bottom: 80px;
text-align: center;
color: white;
background-color: rgb(0,0,0);
/* height: 20vh; */
font-family: Helvetica;
font-size: clamp(14px,1.1vw, 18px);
}
.title-title { .title-title {
font-family: Helvetica; font-family: Helvetica;
color: #fff; color: #fff;
margin-top: 40px; font-size: clamp(12px,1.5vw, 24px);
font-size: clamp(18px,1.5vw, 24px);
} }
.production-credits { .production-credits {
@ -203,25 +180,25 @@ html {
content: '\00a0\00a0\00a0\00a0\00a0\00a0'; /* 6 non-breaking spaces */ content: '\00a0\00a0\00a0\00a0\00a0\00a0'; /* 6 non-breaking spaces */
} }
.stave-wrapper {
.stave-svg {
position: relative; position: relative;
z-index: 1; background-color: rgb(24,24,24);
border: 1px solid rgb(24,24,24);
padding-top: 30px;
padding-bottom: 20px;
} }
.time-indicator {
#time-indicator {
position: absolute;
top: 0; top: 0;
bottom: 0; left: 0;
position: absolute;
width: 2px; width: 2px;
height: 92%; /* height: 100%; */
background-color: rgb(0, 0, 0);
z-index: 0;
animation: moveRight 10s linear infinite; animation: moveRight 10s linear infinite;
will-change: transform; z-index: 2;
} }
.fade-in { .fade-in {
opacity: 1; opacity: 1;
} }
@ -239,7 +216,6 @@ html {
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
@keyframes moveRight { @keyframes moveRight {
0% { 0% {
left: 0; left: 0;