Skip to content

The Arts Incubator

Winnipeg, Manitoba

The project is grounded in a dynamic process of collaborative engagement and capacity building, utilizing arts-based research methodologies to ensure the work is both relevant and empowering. A key focus is Youth Participatory Action Research (YPAR), which positions young people as leaders in investigating their own economic realities and co-designing their futures. Through a series of co-design workshops, digital storytelling projects, and community forums, ECO-STAR North facilitates intergenerational knowledge transfer, connecting youth with Elders and established creators. This hands-on, community-led approach ensures the resulting toolkit is not an academic exercise, but a living, practical resource built by and for Northern innovators, strengthening a resilient and interconnected creative ecosystem.
Primary Menu
  • Home
  • About
    • Winnipeg, Manitoba
    • Art Borups Corners
    • Artists, Collaborators And Mentors
    • Hubs
      • Borups Corners
      • Dyment Recreation Hall and Complex
      • Minneapolis, Minnesota
    • Funders and Supporters
      • Canada Council for the Arts
      • Global Dignity Canada
      • Local Services Board of Melgund
      • Manitoba Arts Council
      • Minneapolis College of Art and Design
    • Reports
      • 2023-2024 Report
      • 2021-2022 Report
    • Sustainable Development Goals (SDG) Tracker
    • Resources
      • Adaptive Phased Management
      • Climate CO-STAR Builder (ECO_STAR)
      • Entrepreneurship Resources
      • Framework for Recreation in Canada
      • Funding Programs and Sources
      • Parks for All
      • The Common Vision
  • Projects
    • Books and Short Stories
      • Barnes and Noble
      • Boekholt Boekhandels
      • eBook.de
      • Ex Libris
      • Fnac
      • Hugendubel
      • LaFeltrinelli Internet Bookshop
      • Lehmanns Media
      • Osiander
      • Palace Marketplace
      • Morawa
      • Orell Füssli
      • Standaard Boekhandel
      • Thalia
    • Food Security
      • Come Eat With Me Manitoba Cookbook
      • Towards a Framework for Northern Food Systems Innovation
      • Food Preservation Training and Curriculum Development
      • Relationship Development and Engagement with the Minneapolis College of Art and Design and University of Minnesota Duluth
      • Relationship Development and Engagement Activities with the University of the Arctic
      • The Art of Canning and Creative Entrepreneurship
    • Incubating Artificial Intelligence
      • Artist Bio Builder Writing Tool
      • Art Idea Generator
      • Asteroids
      • ECO-STAR North
      • Inuit Innovators
      • Step Inside Your Content
      • The Creative Entrepreneurship CO-STAR Guide
      • Unfinished Tales: Methods in Generative Storywork
    • Media Arts and Storytelling
    • Melgund Township Oral History Project
    • Recreation
      • Art Borups Corners
      • Arts and Recreation for an Aging Population
      • Creative Arts for Community Recreation
      • Facilities
        • The Cook Shack
        • Dyment Recreation Hall
        • Melgund Lake Boat Launch
        • Ice Fishing Shack
    • Stories & Publishing Skills
    • Youth Engagement
  • News
    • Borups Corners News
    • Creative Entrepreneurship
    • Artificial Intelligence
    • Arts & Creative Leadership
    • Food Security and Innovation
    • Melgund Township News
    • Photos and Short Stories
    • Winnipeg
  • Events
    • Canada Day 2025
    • 2025-2026 Melgund Township Music Series
  • Contact
  • Home
  • Artificial Intelligence
  • Interstellar Beats: Visualizing Music in a 3D Space
  • Artificial Intelligence

Interstellar Beats: Visualizing Music in a 3D Space

Learn how to make a 3D music visualizer developed using three.js. Bouncing equalizer bars, orbiting camera, and cosmic stars merge sound and art.
Tony Eetak March 9, 2025
Learn how to make a 3D music visualizer developed using three.js. Bouncing equalizer bars, orbiting camera, and cosmic stars merge sound and art.

Learn how to make a 3D music visualizer developed using three.js. Bouncing equalizer bars, orbiting camera, and cosmic stars merge sound and art.

Youth Artists and Musicians Transform Sound into Bouncing Equalizer Bars and Cosmic Starscapes with Three.js

A burst of creative energy is lighting up our screens as part of our 2025 Winter Arts Incubator Program. Musicians and visual artists have joined forces, supported by funding from the OpenAI Researcher Access Program and the Manitoba Arts Council, to explore new ways of experiencing sound through light and motion. This year, we’ve been learning how to transform music with a vibrant 3D equalizer where bouncing bars, swirling stars, and an orbiting camera create an immersive, kinetic visualization of every beat.

Many people assume that this project is all about coding, but it’s much more than that. With the help of AI tools from OpenAI, we use “robot assistants” to generate the code, which frees us to focus on the creative aspects of our work. This approach teaches us to learn the necessary techniques ourselves while encouraging us to innovate. In reality, it took hours to compose the music—an exercise in creativity and emotion—while the equalizer code was generated in not even a few minutes. Along the way, we’re also learning invaluable lessons about digital arts, interactivity, design, and creating experiences that merge art and technology.

In this interactive display, three concentric rings of colored bars respond dynamically to different ranges of musical frequencies. As the music pulses through the speakers, each bar bounces with energy and then gently falls back, creating an exhilarating visual rhythm that mirrors the progression of the song. An auto-rotating camera offers a continuously shifting perspective, allowing viewers to experience the digital symphony from every angle, while a star field in the background adds a cosmic sense of wonder.

The project harnesses the power of three.js to bring these stunning effects to life on the web. Using advanced animation techniques that mimic spring-like bounces and graceful decays, together, we crafted an environment where art and music collide in real time. Every element—from the interactive splash screen featuring logos of our supporting organizations to the replay and next song options—reflects a commitment to making creative technology accessible and engaging.

This easy-to-do project is just one of many innovative activities we learned about this season. We’ve had a lot of fun working with artists and musicians who are eager to push the boundaries of traditional art and performance. We invite others to explore, experiment, and create their own interactive installations—proving that with a bit of code and a lot of imagination, anyone can turn music into a truly visual adventure without having to spend lots of time writing code!

Enjoy the journey into this electrifying fusion of visuals and sound and get inspired to create your own artistic adventures.

Here is the code for making the equalizer. You can try it out by clicking here.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Equalizer – Falling Bars, Orbiting Camera, Stars & Splash</title>
    <style>
      body {
        margin: 0;
        overflow: hidden;
        background: #000;
        color: #fff;
        font-family: sans-serif;
      }
      /* Splash screen overlay */
      #overlay {
        position: fixed;
        inset: 0;
        background: url('splash.jpeg') center/cover;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        z-index: 10;
        text-shadow: 0 0 5px #000;
        padding: 20px;
      }
      /* A two-column row for logos inside the splash screen */
      .logo-row {
        display: flex;
        flex-direction: row;
        justify-content: space-around;
        align-items: center;
        width: 50%;
        margin: 20px 0;
      }
      .logo-row img {
        max-width: 40%;
        height: auto;
        object-fit: contain;
      }
      /* START button styling for splash screen */
      #overlay button {
        padding: 16px 32px;
        font-size: 28px;
        border: none;
        border-radius: 8px;
        background: #000a;
        color: #fff;
        cursor: pointer;
        margin: 20px 0;
      }
      /* Media query to increase button size on mobile */
      @media (max-width: 600px) {
        #overlay button {
          padding: 20px 40px;
          font-size: 32px;
        }
      }
      /* Replay overlay container */
      #replayOverlay {
        position: fixed;
        inset: 0;
        pointer-events: none;
        display: flex;
        flex-direction: column;
        justify-content: center;
        align-items: center;
        z-index: 5;
        background: transparent;
      }
      /* Styling for buttons in the replay overlay */
      #replayOverlay button {
        pointer-events: auto;
        padding: 16px 32px;
        font-size: 28px;
        background: #000a;
        color: #fff;
        border-radius: 8px;
        border: none;
        cursor: pointer;
        margin: 10px;
      }
      /* Hide the built-in play button */
      #playBtn {
        display: none;
      }
    </style>
  </head>
  <body>
    <!-- Splash Screen Overlay -->
    <div id="overlay">
      <h1>Song 2</h1>
      <button onclick="start()">START</button>
      <p>Music by Tony Eetak</p>
      <p><br/><br/>A project supported by:</p>
      <!-- Two-column row for logos -->
      <div class="logo-row">
        <img src="manitoba-arts-council-logo.png" alt="A Project supported by the Manitoba Arts Council">
        <img src="powered-by-openai-badge-filled-on-dark.png" alt="A Project Made Possible with support from the OpenAI Researcher Access Program">
      </div>
    </div>
    
    <!-- Replay Overlay (hidden initially) -->
    <div id="replayOverlay" style="display:none;">
      <button onclick="replay()">Replay Song</button>
      <button onclick="nextSong()">Next Song</button>
    </div>
    
    <!-- A hidden built-in play button (unused) -->
    <button id="playBtn">Play Music</button>
    
    <!-- Load Three.js r128 -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <!-- Load OrbitControls -->
    <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
    <script>
      /*
        This version creates an equalizer with three rings of bouncing bars,
        an orbiting camera, and a distant star field.
        When the page loads, a splash overlay with a title, a two‑column logo row,
        a large START button, and credits is displayed.
      
        When the song ends, a replay overlay appears with two buttons: one to replay
        the current song and another ("Next Song") that sends the user to "/music/song1".
      */
      
      // GLOBAL VARIABLES
      let scene, camera, renderer, controls;
      const numBars = 32;  // per ring
      const lowBars = [], midBars = [], highBars = [];
      const lowRadius = 80, midRadius = 160, highRadius = 240;
      
      // AUDIO VARIABLES
      let audioCtx, analyser, audioBuffer, audioSource;
      let playing = false;
      let freqData;
      const FFT_SIZE = 1024; // yields 512 bins
      const totalBlocks = numBars * 3;
      
      // Equalizer parameters
      const gainLow  = 150;
      const gainMid  = 150;
      const gainHigh = 400;
      const alpha = 8;
      const lowMaxHeight = 60;
      const midMaxHeight = 60;
      const highMaxHeight = 80;
      
      // SPRING/DAMPER parameters (active & fall-down)
      const springStrength = 0.5;
      const damping = 0.08;
      
      // BOUNCE parameters (active only)
      const bounceFrequency = 5;
      const bounceAmplitudeFactor = 0.5;
      
      init();
      animate();
      
      function init(){
        // Setup scene, camera, renderer.
        scene = new THREE.Scene();
        scene.background = new THREE.Color(0x000000);
        camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 1, 4000);
        camera.position.set(0, 300, 400);
        renderer = new THREE.WebGLRenderer({antialias: true});
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);
      
        // Setup OrbitControls with autoRotate.
        controls = new THREE.OrbitControls(camera, renderer.domElement);
        controls.autoRotate = true;
        controls.autoRotateSpeed = 2.0;
      
        // Lighting.
        scene.add(new THREE.AmbientLight(0x444444));
        const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
        directionalLight.position.set(1, 1, 1);
        scene.add(directionalLight);
      
        // Optional grid helper.
        const gridHelper = new THREE.GridHelper(600, 30, 0x444444, 0x222222);
        scene.add(gridHelper);
      
        // Create equalizer rings.
        createRing(lowBars, numBars, lowRadius, 240, 0, gainLow);
        createRing(midBars, numBars, midRadius, 120, 60, gainMid);
        createRing(highBars, numBars, highRadius, 280, 180, gainHigh);
        lowBars.forEach(bar => scene.add(bar));
        midBars.forEach(bar => scene.add(bar));
        highBars.forEach(bar => scene.add(bar));
      
        // Add star field.
        const stars = createStarField(1000);
        scene.add(stars);
      
        // Setup audio context and analyser.
        audioCtx = new (window.AudioContext || window.webkitAudioContext)();
        analyser = audioCtx.createAnalyser();
        analyser.fftSize = FFT_SIZE;
        freqData = new Uint8Array(analyser.frequencyBinCount);
      
        // Load audio file (update URL as needed).
        fetch('song-4.m4a')
          .then(response => response.arrayBuffer())
          .then(arrayBuffer => audioCtx.decodeAudioData(arrayBuffer))
          .then(decodedData => { audioBuffer = decodedData; });
      
        window.addEventListener('resize', onWindowResize, false);
      }
      
      // createRing builds a ring of bars arranged in a circle.
      function createRing(arr, count, radius, baseHue, targetHue, gain) {
        for (let i = 0; i < count; i++){
          const geometry = new THREE.BoxGeometry(10, 1, 10);
          geometry.translate(0, 0.5, 0);
          const material = new THREE.MeshPhongMaterial({ color: new THREE.Color().setHSL(baseHue/360, 1, 0.5) });
          const bar = new THREE.Mesh(geometry, material);
          const angle = (i / count) * 2 * Math.PI;
          bar.position.x = Math.cos(angle) * radius;
          bar.position.z = Math.sin(angle) * radius;
          bar.rotation.y = -angle;
          bar.userData = {
            baseHue: baseHue,
            targetHue: targetHue,
            gain: gain,
            velocity: 0,
            phase: Math.random() * Math.PI * 2
          };
          arr.push(bar);
        }
      }
      
      // createStarField generates a point-based star field in a spherical shell.
      function createStarField(starCount) {
        const geometry = new THREE.BufferGeometry();
        const positions = [];
        const minRadius = 800, maxRadius = 2000;
        for (let i = 0; i < starCount; i++){
          const r = THREE.MathUtils.randFloat(minRadius, maxRadius);
          const theta = THREE.MathUtils.randFloat(0, Math.PI);
          const phi = THREE.MathUtils.randFloat(0, Math.PI * 2);
          const x = r * Math.sin(theta) * Math.cos(phi);
          const y = r * Math.sin(theta) * Math.sin(phi);
          const z = r * Math.cos(theta);
          positions.push(x, y, z);
        }
        geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
        const material = new THREE.PointsMaterial({ color: 0xffffff, size: 2, sizeAttenuation: true });
        return new THREE.Points(geometry, material);
      }
      
      // updateEqualizer processes FFT data and updates each bar’s height and color while the song plays.
      function updateEqualizer(){
        analyser.getByteFrequencyData(freqData);
        let overallSum = 0;
        for (let i = 0; i < freqData.length; i++){
          overallSum += freqData[i];
        }
        const overallAvg = overallSum / freqData.length;
        const binsPerBlock = freqData.length / totalBlocks;
        const allBlocks = lowBars.concat(midBars, highBars);
        const t = performance.now() / 1000;
        for (let blockIndex = 0; blockIndex < totalBlocks; blockIndex++){
          const startBin = Math.floor(blockIndex * binsPerBlock);
          const endBin = Math.floor((blockIndex + 1) * binsPerBlock);
          let sum = 0, count = 0;
          for (let j = startBin; j < endBin; j++){
            sum += freqData[j] || 0;
            count++;
          }
          const assignedAvg = count ? (sum / count) : 0;
          const mixedAvg = 0.5 * assignedAvg + 0.5 * overallAvg;
          let norm = 1 - Math.exp(-alpha * (mixedAvg / 255));
          norm = Math.min(Math.max(norm, 0), 1);
          
          let block, gain, maxHeight;
          if (blockIndex < numBars) {
            block = lowBars[blockIndex];
            gain = block.userData.gain;
            maxHeight = lowMaxHeight;
          } else if (blockIndex < numBars * 2) {
            block = midBars[blockIndex - numBars];
            gain = block.userData.gain;
            maxHeight = midMaxHeight;
          } else {
            block = highBars[blockIndex - numBars * 2];
            gain = block.userData.gain;
            maxHeight = highMaxHeight;
          }
          
          let targetHeight = 1 + norm * gain;
          targetHeight = Math.min(targetHeight, maxHeight);
          let diff = targetHeight - block.scale.y;
          block.userData.velocity += diff * springStrength;
          block.userData.velocity *= (1 - damping);
          block.scale.y += block.userData.velocity;
          if (block.scale.y < 1){
            block.scale.y = 1;
            block.userData.velocity = 0;
          }
          
          // Add bounce oscillation.
          const bounce = Math.sin(t * bounceFrequency + block.userData.phase) * ((targetHeight - 1) * bounceAmplitudeFactor);
          let finalHeight = block.scale.y + bounce;
          finalHeight = Math.max(1, Math.min(finalHeight, maxHeight));
          block.scale.y = finalHeight;
          
          let hue = block.userData.baseHue + (block.userData.targetHue - block.userData.baseHue) * norm;
          block.material.color.setHSL(hue/360, 1, 0.5);
        }
      }
      
      // updateFallDown makes bars fall back to a baseline (height 1) when the song ends.
      function updateFallDown(){
        const allBlocks = lowBars.concat(midBars, highBars);
        for (let i = 0; i < allBlocks.length; i++){
          const block = allBlocks[i];
          let diff = 1 - block.scale.y;
          block.userData.velocity += diff * springStrength;
          block.userData.velocity *= (1 - damping);
          block.scale.y += block.userData.velocity;
          if (block.scale.y < 1){
            block.scale.y = 1;
            block.userData.velocity = 0;
          }
          block.material.color.setHSL(block.userData.baseHue/360, 1, 0.5);
        }
      }
      
      function animate(){
        requestAnimationFrame(animate);
        controls.update();
        if (playing) {
          updateEqualizer();
        } else {
          updateFallDown();
        }
        renderer.render(scene, camera);
      }
      
      function onWindowResize(){
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(window.innerWidth, window.innerHeight);
      }
      
      // Called when the splash "START" button is pressed.
      function start(){
        document.getElementById('overlay').style.display = "none";
        if (!playing && audioBuffer) {
          if (audioCtx.state === 'suspended') audioCtx.resume();
          audioSource = audioCtx.createBufferSource();
          audioSource.buffer = audioBuffer;
          audioSource.connect(analyser);
          audioSource.connect(audioCtx.destination);
          audioSource.start();
          playing = true;
          audioSource.onended = () => {
            playing = false;
            document.getElementById('replayOverlay').style.display = "flex";
          };
        }
      }
      
      // Called when the replay button is pressed.
      function replay(){
        document.getElementById('replayOverlay').style.display = "none";
        start();
      }
      
      // Called when the Next Song button is pressed.
      // Here, we simply redirect to '/music/song1'.
      function nextSong(){
        window.location.href = "/music/song1";
      }
    </script>
  </body>
</html>

About the Author

Tony Eetak

Tony Eetak

Administrator

Tony Eetak is an emerging artist, musician and culture connector from Arviat, Nunavut, now exploring the arts in Winnipeg, Manitoba. A founding member of the Art Borups Corners, Tony has a demonstrated passion for photography, music, composition, and visual arts. With over five years of experience as a dedicated volunteer, collaborator and co-funder of several arts projects, Tony has been involved in various participatory arts events through organizations like the Arviat Film Society, Global Dignity Canada, Inclusion in Northern Research, and Our People, Our Climate. His contributions earned him recognition as a National Role Model by Global Dignity Canada in 2023. His work has been supported by the Canada Council for the Arts, Manitoba Arts Council and the OpenAI Researcher Access Program.

Visit Website View All Posts
Tags: 2024-5782 Manitoba Manitoba Artists Manitoba Arts Council Manitoba Arts Program SDG 4 Winnipeg Manitoba

Post navigation

Previous: Sharing but not reading: Depth in a Digital Age
Next: Sustaining Community Programs

Related News

Multi-agent systems rely on specialist agents — AI systems dedicated to specific types of work. These can include text generation, image synthesis, data analysis, or even music composition. Each agent is designed to perform a narrow but complex task exceptionally well.
  • Artificial Intelligence

AI Agents: Specialists at Work

The Arts Incubator - Winnipeg November 12, 2025
At its core, an AI “agent” is not sentient. It’s a system capable of perceiving its environment, planning a sequence of actions, and executing those actions to reach a goal. In a multi-agent system, these behaviors are distributed across the team.
  • Artificial Intelligence

Research: When AI Becomes a Team

Jamie Bell November 10, 2025
Multi-agent AI represents a new paradigm: one where creativity, reliability, and collaboration converge. These systems do not replace human imagination; they expand it, enabling us to tackle ambitious projects with confidence.
  • Artificial Intelligence

Designing the Future

The Arts Incubator - Winnipeg November 8, 2025

Recent Posts

  • AI Agents: Specialists at Work
  • Research: When AI Becomes a Team
  • AI and the Arts
  • Agentic Design? So, Where’s the Art?
  • Beyond Text Generation

You may have missed

Multi-agent systems rely on specialist agents — AI systems dedicated to specific types of work. These can include text generation, image synthesis, data analysis, or even music composition. Each agent is designed to perform a narrow but complex task exceptionally well.
  • Artificial Intelligence

AI Agents: Specialists at Work

The Arts Incubator - Winnipeg November 12, 2025
At its core, an AI “agent” is not sentient. It’s a system capable of perceiving its environment, planning a sequence of actions, and executing those actions to reach a goal. In a multi-agent system, these behaviors are distributed across the team.
  • Artificial Intelligence

Research: When AI Becomes a Team

Jamie Bell November 10, 2025
Practical workflows are also changing the conversation. Artists are increasingly using AI to handle repetitive or large-scale tasks, freeing humans to focus on storytelling and interpretation.
  • Creative Entrepreneurship

AI and the Arts

Jamie Bell November 9, 2025
It’s easy to look at these pipelines and think that what we’re building is engineering, not creativity.
  • Arts & Creative Leadership

Agentic Design? So, Where’s the Art?

Jamie Bell November 9, 2025

MANITOBA ARTS PROGRAMS

This platform, our Winnipeg, Manitoba hub and programs have been made possible with support from the Manitoba Arts Council Indigenous 360 Program. We gratefully acknowledge their funding and support in making the work we do possible.

Manitoba Arts Council Indigenous 360 Program

ACKNOWLEDGEMENTS

The Arts Incubator was seeded and piloted with strategic arts innovation funding from the Canada Council for the Arts Digital Greenhouse. We thank them for their investment, supporting northern arts capacity building and bringing the arts to life.

Canada Council for the Arts Digital Greenhouse Logo

NORTHWESTERN ONTARIO ARTS

This platform, our Northwestern Ontario hub and programs have been made possible with support from the Ontario Arts Council Multi and Inter-Arts Projects Program. We gratefully acknowledge their funding and support in making the work we do possible.

Ontario Arts Council Multi and Inter-Arts Projects Program
Copyright © All rights reserved. | MoreNews by AF themes.