Compare commits

...

15 Commits

Author SHA1 Message Date
joe 28a2c31239 str 2025-04-25 13:04:02 +01:00
joe e5eacbba0d This is a working performance model 2025-04-25 09:03:54 +01:00
joe 0e31c0ea1d layout done 2025-04-24 13:19:06 +01:00
Joe e6ed178aac sorted mime type problem 2025-04-24 10:46:45 +01:00
Joe 10b8111517 should be good 2025-04-24 09:25:37 +01:00
Joe 9de15ba9f8 should be good 2025-04-24 09:24:48 +01:00
Joe f50bd236ca rendering correct dimentions 2025-04-23 21:02:34 +01:00
Joe 8993615953 nearly sorted with the rendering 2025-04-23 12:32:44 +01:00
Joe 75b66e245e this is garbage from chatty mc fuckin chat face 2025-04-18 14:35:30 +01:00
Joe f776fe377a a little progress 2025-04-18 03:57:30 +01:00
Joe be327165b8 ugh? 2025-04-16 03:25:55 +01:00
Joe c6c659535e sorted the title updates 2025-04-15 04:35:56 +01:00
Joe 17f1cbfa9b refactored - still some renedering issue 2025-04-14 05:06:59 +01:00
Joe ac58c0e8c3 startng to make classs - Socket class not okay 2025-04-13 14:26:24 +01:00
Joe 1393acdc42 distributing functions to files to go OO 2025-04-13 13:36:30 +01:00
23 changed files with 527 additions and 88874 deletions

View File

@ -1 +0,0 @@
# conductor

View File

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

Before

Width:  |  Height:  |  Size: 457 B

After

Width:  |  Height:  |  Size: 482 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"><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" 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>

Before

Width:  |  Height:  |  Size: 501 B

After

Width:  |  Height:  |  Size: 526 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"><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" 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>

Before

Width:  |  Height:  |  Size: 396 B

After

Width:  |  Height:  |  Size: 422 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"><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" 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>

Before

Width:  |  Height:  |  Size: 473 B

After

Width:  |  Height:  |  Size: 498 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"><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" 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>

Before

Width:  |  Height:  |  Size: 507 B

After

Width:  |  Height:  |  Size: 533 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"><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" 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>

Before

Width:  |  Height:  |  Size: 439 B

After

Width:  |  Height:  |  Size: 465 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"><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" 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>

Before

Width:  |  Height:  |  Size: 662 B

After

Width:  |  Height:  |  Size: 688 B

View File

@ -1 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 330.51 33.72"><defs><style>.cls-1{stroke:#828383;stroke-width:.5px;}.cls-1,.cls-2{fill:none;}.cls-3{clip-path:url(#clippath-2);}.cls-2{stroke-width:0px;}.cls-4{clip-path:url(#clippath-1);}.cls-5{opacity:.75;}.cls-6{clip-path:url(#clippath);}</style><clipPath id="clippath"><rect class="cls-2" width="330.51" height="33.72"/></clipPath><clipPath id="clippath-1"><rect class="cls-2" y="0" width="330.51" height="33.72"/></clipPath><clipPath id="clippath-2"><rect class="cls-2" x="-25" y="-22" width="380.51" height="56.72"/></clipPath></defs><g id="Layer_1-2"><g class="cls-6"><g class="cls-4"><g class="cls-5"><g class="cls-3"><rect class="cls-1" x=".25" y=".25" width="33" height="33.22"/><rect class="cls-1" x="132.26" y=".25" width="33" height="33.22"/><rect class="cls-1" x="66.26" y=".25" width="33" height="33.22"/><rect class="cls-1" x="198.27" y=".25" width="33" height="33.22"/><rect class="cls-1" x="264.26" y=".25" width="33" height="33.22"/><rect class="cls-1" x="33.26" y=".25" width="33" height="33.22"/><rect class="cls-1" x="165.27" y=".25" width="33" height="33.22"/><rect class="cls-1" x="99.25" y=".25" width="33" height="33.22"/><rect class="cls-1" x="231.27" y=".25" width="33" height="33.22"/><rect class="cls-1" x="297.26" y=".25" width="33" height="33.22"/><rect class="cls-1" x=".25" y=".25" width="330.01" height="6.56"/><rect class="cls-1" x=".25" y="13.58" width="330.01" height="6.56"/><rect class="cls-1" x=".25" y="6.91" width="330.01" height="6.56"/><rect class="cls-1" x=".25" y="20.24" width="330.01" height="6.56"/><rect class="cls-1" x=".25" y="26.91" width="330.01" height="6.56"/></g></g></g></g></g></svg>
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 330.51 33.72" ><defs><style>.cls-1{stroke:#828383;stroke-width:.5px;}.cls-1,.cls-2{fill:none;}.cls-3{clip-path:url(#clippath-2);}.cls-2{stroke-width:0px;}.cls-4{clip-path:url(#clippath-1);}.cls-5{opacity:.75;}.cls-6{clip-path:url(#clippath);}</style><clipPath id="clippath"><rect class="cls-2" width="330.51" height="33.72"/></clipPath><clipPath id="clippath-1"><rect class="cls-2" y="0" width="330.51" height="33.72"/></clipPath><clipPath id="clippath-2"><rect class="cls-2" x="-25" y="-22" width="380.51" height="56.72"/></clipPath></defs><g id="Layer_1-2"><g class="cls-6"><g class="cls-4"><g class="cls-5"><g class="cls-3"><rect class="cls-1" x=".25" y=".25" width="33" height="33.22"/><rect class="cls-1" x="132.26" y=".25" width="33" height="33.22"/><rect class="cls-1" x="66.26" y=".25" width="33" height="33.22"/><rect class="cls-1" x="198.27" y=".25" width="33" height="33.22"/><rect class="cls-1" x="264.26" y=".25" width="33" height="33.22"/><rect class="cls-1" x="33.26" y=".25" width="33" height="33.22"/><rect class="cls-1" x="165.27" y=".25" width="33" height="33.22"/><rect class="cls-1" x="99.25" y=".25" width="33" height="33.22"/><rect class="cls-1" x="231.27" y=".25" width="33" height="33.22"/><rect class="cls-1" x="297.26" y=".25" width="33" height="33.22"/><rect class="cls-1" x=".25" y=".25" width="330.01" height="6.56"/><rect class="cls-1" x=".25" y="13.58" width="330.01" height="6.56"/><rect class="cls-1" x=".25" y="6.91" width="330.01" height="6.56"/><rect class="cls-1" x=".25" y="20.24" width="330.01" height="6.56"/><rect class="cls-1" x=".25" y="26.91" width="330.01" height="6.56"/></g></g></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

File diff suppressed because one or more lines are too long

View File

@ -15,11 +15,6 @@
<div class="loading">
</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">
@ -38,56 +33,6 @@
id="time-indicator"></object>
</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>
</body>

View File

@ -1,18 +0,0 @@
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

45
src/audio.js Normal file
View File

@ -0,0 +1,45 @@
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();
}
}

62
src/conductor.js Normal file
View File

@ -0,0 +1,62 @@
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,62 +3,67 @@ async function loadInstrumentsConfig() {
return await response.json();
}
async function fetchAudioData(url) {
async function fetchSvgText(url) {
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();
return arrayBuffer;
return await response.text();
}
async function decodeAudioData(context, arrayBuffer) {
function svgTextToImage(svgText, defaultWidth = 100, defaultHeight = 100) {
return new Promise((resolve, reject) => {
context.decodeAudioData(arrayBuffer, resolve, reject);
});
}
// Create a blob from the SVG text
const blob = new Blob([svgText], { type: "image/svg+xml" });
const url = URL.createObjectURL(blob);
const img = new Image();
async function loadAudioSamples(context, directory, filenames) {
const audioFiles = filenames;
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;
// 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;
}
return audioBuffers;
}
// Clean up the object URL to free memory
URL.revokeObjectURL(url);
async function fetchImageData(url) {
const response = await fetch(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
resolve(img);
};
// Handle image loading errors
img.onerror = (err) => {
URL.revokeObjectURL(url); // Clean up URL
reject(err);
};
img.src = url;
});
}
export async function loadInstruments() {
const instrumentsConfig = await loadInstrumentsConfig();
const context = new (window.AudioContext || window.webkitAudioContext)();
const instruments = {};
for (const [instrument, config] of Object.entries(instrumentsConfig)) {
const samples = await loadAudioSamples(
context,
config.directory,
config.filenames
);
const imageData = await fetchImageData(
const numSamples = config.filenames.length;
const svgText = await fetchSvgText(
`${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] = {
samples: samples,
image: imageData,
yPos: config.image.yPos, // Store base64-encoded image data
numSamples: numSamples,
image: img,
yPos: config.image.yPos,
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 ) {
export async function sendMidiMessage(midiChannelName, sampleNumber, midiAccess) {
try {
const midiAccess = await navigator.requestMIDIAccess();
console.log(midiChannelName, sampleNumber);
const availableMidiOutputs = midiAccess.outputs.values();
let midiOut;
@ -9,21 +9,20 @@ export async function sendMidiMessage(midiChannelName, sampleNumber ) {
return;
}
for ( const availableMidiOutput of availableMidiOutputs){
if (availableMidiOutput.name === midiChannelName){
for (const availableMidiOutput of availableMidiOutputs) {
if (availableMidiOutput.name === midiChannelName) {
midiOut = availableMidiOutput;
break;
}
}
// MIDI Note On (Play Note)
midiOut.send([0x90, 36 + sampleNumber, 127]);
midiOut.send([0x90, 35 + sampleNumber, 127]);
// MIDI Note Off after 500ms
setTimeout(() => {
midiOut.send([0x80, 36 + sampleNumber, 0]); // Stop Note
midiOut.send([0x80, 35 + sampleNumber, 0]); // Stop Note
}, 500);
} catch (error) {
console.error("Failed to get MIDI access:", error);
}

135
src/render.js Normal file
View File

@ -0,0 +1,135 @@
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,294 +1,44 @@
import { Client, BrowserSocketFactory } from "./blitz.js";
import { loadInstruments } from "./instruments.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 () => {
let showing;
let audioContext;
let interacted = true;
let gainNode;
sendMidiMessage();
const midiAccess = await navigator.requestMIDIAccess();
const dataStream = new DataStream();
const conductor = new Conductor();
await conductor.init();
const instruments = await conductor.instruments;
let strikes = 0;
const strikeEvery = 2;
function startAudio() {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
gainNode = audioContext.createGain();
gainNode.gain.value = 0;
gainNode.connect(audioContext.destination);
const strikeHandler = () => {
console.log(strikes);
//** select instrument */
if (strikes === strikeEvery){
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 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);
const renderer = new Renderer(dataStream, strikeHandler);
// source.buffer = samples[`${randomKey}`];
// source.connect(gainNode);
// source.start();
}
};
dataStream.addEventListener("strike", strikeHandler);
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;
dataStream.init();
// 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
conductor.showMainContent();
});

71
src/socket.js Normal file
View File

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