Media Art ProgrammingWeek 14 · Period 03 Handout

p5.js / Media Art Programming

14주차 3교시 — 사운드 비주얼 실습 + 기말 프로젝트 구상

Sound, Media, and Final Project 주제의 학생용 강의 자료입니다.

WEEK 14 PERIOD 03 P5.JS MEDIA ART

14주차 3교시 — 사운드 비주얼 실습 + 기말 프로젝트 구상

강의 아웃라인: week-14-outline 이전 교시: week-14-period2-student (FFT + 이미지/웹캠 + 기말 안내)

항목 내용
대상 P5.js 입문자 (테크아트 전공, 1·2교시 Amplitude·FFT·미디어 학습 완료)
소요 시간 45분 (Part A 25 + Part B 15 + 마무리 5)
학습 목표
  • 1~2교시 사운드 분석을 직접 구현해 비주얼에 매핑할 수 있습니다
  • 기말 프로젝트 주제·방향·기술을 정해 한 장으로 구상할 수 있습니다
실습 안내

제출·채점하는 과제가 아닌 수업 내 실습입니다. 14주차는 학기 마지막 정규 수업이며, 결과물은 기말 프로젝트의 출발점으로 재활용할 수 있습니다.


실습 환경 안내

3교시는 두 파트로 나뉩니다. Part A 25분 — 사운드 비주얼을 직접 만들기, Part B 15분 — 기말 프로젝트를 구상하기, 마지막 마무리 5분.

파트 시간 내용
Part A 25분 사운드 비주얼 실습 — 1~2교시 내용 직접 구현
Part B 15분 기말 프로젝트 구상 — 주제·방향·기술 선정
마무리 5분 저장 + 기말 일정 재확인 + 학기 회고

사운드 파일 준비

Web Editor에 음악 파일을 업로드합니다. 자기가 좋아하는 곡(MP3, 30초~1분 권장)이 있으면 가져와도 되고, 없으면 강사가 준비한 샘플 파일을 씁니다. 비트감 있고 음량 변화가 뚜렷한 곡이 효과 확인에 좋습니다.

마이크 사용을 원하면: 1교시 핵심 5에서 본 p5.AudioIn 패턴을 그대로 씁니다. 브라우저 마이크 권한 허용이 필요합니다. 노이즈가 많은 환경에서는 파일 기반이 더 안정적입니다.

시작 힌트

1교시 완성 코드를 복사해 붙여넣고 거기서 시작하는 게 가장 안전합니다. 빈 캔버스부터 시작하지 말고, 돌아가는 코드 위에서 수정합니다.

방향 A/B/C 중 하나를 골라 시작합니다. 가장 쉬운 건 방향 A이고, 막막하면 A부터 시작합니다.


Part A — 사운드 비주얼 실습

방향 A 스타터: 음량 반응 비주얼 (p5.Amplitude)

1교시에서 만든 코드를 기반으로 시작합니다.

// === 방향 A: 음량 반응 비주얼 ===
// 음량에 따라 원의 크기와 색상이 변합니다
// 여기서 시작해서 자유롭게 수정하세요

let song;
let amp;

function preload() {
  // 자신의 음악 파일명으로 바꾸세요
  song = loadSound('assets/music.mp3');
}

function setup() {
  createCanvas(400, 400);
  amp = new p5.Amplitude();
}

function draw() {
  background(0);

  let level = amp.getLevel(); // 0.0 ~ 1.0

  // 음량 → 크기
  let size = map(level, 0, 1, 10, 400);
  // 음량 → 색상
  let r = map(level, 0, 1, 50, 255);
  let b = map(level, 0, 1, 255, 50);

  fill(r, 50, b);
  noStroke();
  ellipse(width / 2, height / 2, size, size);
}

function mousePressed() {
  userStartAudio();
  // 재생 토글 — 반복 클릭 시 중복 재생 방지
  if (song.isPlaying()) {
    song.pause();
  } else {
    song.loop();
  }
}

도전 옵션.

  1. 원 대신 다른 도형(사각형·삼각형) 사용
  2. 잔상 효과 추가: background(0, 0, 0, 30) — 5주차에서 배운 것
  3. 원을 여러 개 그리기: for문(3주차) 활용
  4. 음량에 따라 원의 위치도 변하게 하기
  5. HSB 색상 모드에서 frameCount로 색상 순환: fill(frameCount % 360, 80, 100)

방향 B 스타터: 이퀄라이저 바 차트 (p5.FFT)

// === 방향 B: 이퀄라이저 바 차트 ===
// 주파수 스펙트럼을 막대 차트로 시각화합니다

let song;
let fft;

function preload() {
  song = loadSound('assets/music.mp3');
}

function setup() {
  createCanvas(400, 400);
  colorMode(HSB, 255);
  fft = new p5.FFT();
}

function draw() {
  background(0);

  let spectrum = fft.analyze();

  noStroke();
  for (let i = 0; i < spectrum.length; i++) {
    let x = map(i, 0, spectrum.length, 0, width);
    let h = map(spectrum[i], 0, 255, 0, height);
    fill(map(i, 0, spectrum.length, 0, 255), 255, 255);
    rect(x, height - h, width / spectrum.length, h);
  }
}

function mousePressed() {
  userStartAudio();
  // 재생 토글 — 반복 클릭 시 중복 재생 방지
  if (song.isPlaying()) {
    song.pause();
  } else {
    song.loop();
  }
}

도전 옵션.

  1. 막대 개수 줄이기: new p5.FFT(0.8, 64) — 64개 막대로 더 굵게
  2. 원형 이퀄라이저: translate(width/2, height/2) + rotate() + 막대를 원형 배치
  3. 잔상 효과 추가
  4. 막대 대신 원으로 표현: ellipse(x, height - h, 5, 5)

방향 C 스타터: 저음/중음/고음 분리 (fft.getEnergy())

// === 방향 C: 저음/중음/고음 분리 비주얼 ===
// bass, mid, treble을 각각 다른 도형에 매핑합니다

let song;
let fft;

function preload() {
  song = loadSound('assets/music.mp3');
}

function setup() {
  createCanvas(400, 400);
  fft = new p5.FFT();
}

function draw() {
  background(0);
  fft.analyze(); // 반드시 먼저 호출

  let bass = fft.getEnergy("bass");
  let mid = fft.getEnergy("mid");
  let treble = fft.getEnergy("treble");

  // 저음 → 빨간 원 (왼쪽)
  let bassSize = map(bass, 0, 255, 10, 200);
  fill(255, 50, 50);
  noStroke();
  ellipse(width * 0.2, height / 2, bassSize, bassSize);

  // 중음 → 초록 사각형 (가운데)
  let midSize = map(mid, 0, 255, 10, 150);
  fill(50, 255, 50);
  rectMode(CENTER);
  rect(width * 0.5, height / 2, midSize, midSize);

  // 고음 → 파란 삼각형 (오른쪽)
  let trebleSize = map(treble, 0, 255, 10, 100);
  fill(50, 50, 255);
  triangle(
    width * 0.8, height / 2 - trebleSize / 2,
    width * 0.8 - trebleSize / 2, height / 2 + trebleSize / 2,
    width * 0.8 + trebleSize / 2, height / 2 + trebleSize / 2
  );
}

function mousePressed() {
  userStartAudio();
  // 재생 토글 — 반복 클릭 시 중복 재생 방지
  if (song.isPlaying()) {
    song.pause();
  } else {
    song.loop();
  }
}

도전 옵션.

  1. 각 도형에 투명도 추가: fill(255, 50, 50, map(bass, 0, 255, 50, 200))
  2. 도형 더 추가: lowMid·highMid 밴드 활용
  3. 저음에 반응하는 배경색: background(map(bass, 0, 255, 0, 50), 0, 0)
  4. 13주차 파티클과 결합 (2교시 심화 4 참고)

Step-by-Step 가이드 (방향 A 기준)

Step 1 — 사운드 로드 및 재생 (7분)

목표: 음악 파일이 정상적으로 재생되는지 확인.

  1. P5.js Web Editor에서 새 스케치 생성
  2. 음악 파일을 Web Editor에 업로드 (파일 탭 → Upload file)
  3. 방향 A 스타터 코드 복사·붙여넣기
  4. 파일명을 자신의 파일명으로 수정: loadSound('자신의파일명.mp3')
  5. 실행 → 캔버스 클릭 → 소리가 나는지 확인
소리가 안 나면

userStartAudio()mousePressed() 안에 있는지 → 캔버스(검은 화면)를 클릭했는지 → 파일명 철자·확장자 확인 → 콘솔 에러 메시지 확인. 업로드·권한 지연을 가정해 이 단계에 2분 여유를 두었습니다.

성공 화면

실행하고 캔버스를 클릭하면 음악이 흘러나오고, 음량에 따라 원이 커졌다 작아집니다.

Step 2 — 분석 객체 연결 (8분)

목표: 분석값(amp.getLevel())이 실제 0~1 사이 값으로 들어오는지 확인하고, 곡에 맞춰 매핑 범위를 조정합니다.

콘솔에 음량 값을 찍어 확인합니다.

function draw() {
  background(0);
  let level = amp.getLevel();
  console.log(level); // 콘솔에서 값 확인
  // ... 기존 그리기 ...
}

콘솔을 열어 값이 흘러가는 걸 봅니다. 보통 조용한 부분은 0.01~0.05, 큰 부분은 0.2~0.5 정도. 곡마다 최대값이 다르다는 점이 핵심입니다.

매핑 범위 조정: 원의 변화가 너무 미미하면 map()의 입력 최대값을 줄입니다.

// 변화가 잘 안 보이면 1 → 0.5나 0.3으로
let size = map(level, 0, 0.3, 10, 400);

방향 B/C로 갈 경우: 여기서 fft = new p5.FFT() + fft.analyze()로 객체를 바꾸고 콘솔로 spectrum 배열을 확인합니다.

성공 화면

콘솔에 0~0.5 사이 숫자가 실시간으로 흐르고, 원의 변화 폭이 곡에 맞게 자연스럽습니다.

Step 3 — 비주얼 매핑 (10분)

목표: 분석값을 시각적 속성(크기·색상·위치·잔상)에 자유롭게 연결합니다.

아래 순서로 한 겹씩 쌓습니다.

3-1. 잔상 효과

// background(0); 을 아래로 교체
background(0, 0, 0, 30); // 반투명 배경 → 잔상 효과

3-2. 원을 여러 개 — 동심원

function draw() {
  background(0, 0, 0, 30);

  let level = amp.getLevel();

  for (let i = 0; i < 5; i++) {
    let size = map(level, 0, 0.5, 10, 400 - i * 70);
    let alpha = map(i, 0, 4, 255, 50);
    fill(map(level, 0, 0.5, 50, 255), 50, map(level, 0, 0.5, 255, 50), alpha);
    noStroke();
    ellipse(width / 2, height / 2, size, size);
  }
}

3-3. HSB 색상 + frameCount로 색상 순환

function setup() {
  createCanvas(400, 400);
  colorMode(HSB, 360, 100, 100);
  amp = new p5.Amplitude();
}

function draw() {
  background(0, 0, 0, 30);

  let level = amp.getLevel();
  let size = map(level, 0, 0.5, 10, 400);

  // frameCount로 색상 순환 + 음량으로 채도 조절
  fill(frameCount % 360, map(level, 0, 0.5, 30, 100), 100);
  noStroke();
  ellipse(width / 2, height / 2, size, size);
}

3-4. 인터랙션 추가 (옵션)

// 마우스 위치로 원 위치 변경
ellipse(mouseX, mouseY, size, size);

// 클릭으로 재생/일시정지 토글
function mousePressed() {
  userStartAudio();
  if (song.isPlaying()) {
    song.pause();
  } else {
    song.loop();
  }
}
성공 화면

자신만의 색상·크기·잔상·인터랙션 조합으로 1분 안에 음악에 반응하는 비주얼이 화면을 가득 채웁니다.


Part B — 기말 프로젝트 구상

종이나 노트를 준비합니다.

Step 1 — 주제 선정 (5분)

먼저 이번 학기에서 가장 재미있었던 수업을 떠올려 봅니다.

질문 가이드.

  • 어떤 주차 실습이 제일 재밌었나?
  • 만들어보고 싶었는데 시간이 부족했던 건?
  • OpenProcessing이나 YouTube에서 본 작품 중 따라 해보고 싶었던 게 있는가?

키워드 3개를 종이에 적어 봅니다. 예: ‘파티클 + 마우스 + 색상’ 또는 ‘소리 + 노이즈 + 원형’.

방향 예시 (2교시 안내 복습).

  • A. 사운드 리액티브 비주얼
  • B. 인터랙티브 시뮬레이션 (물리, 중력)
  • C. 데이터 아트
  • D. 제너러티브 아트 (노이즈 기반)
  • E. 자유 주제

Step 2 — 컨셉 스케치 (7분)

키워드를 정했으면, 대략적인 화면 구성을 그려 봅니다. 예쁘게 안 그려도 됩니다. 네모 하나 그리고 그 안에 무엇이 있을지 스케치하면 됩니다.

스케치에 포함할 것.

  • 캔버스에 어떤 요소가 있는지 (도형? 파티클? 이미지?)
  • 어떤 인터랙션이 있는지 (마우스 클릭? 키보드? 소리?)
  • 시간에 따라 어떻게 변하는지

아래 체크리스트에서 쓸 기술 2가지 이상을 체크합니다.

기술 체크리스트

Step 3 — 프로토타입 시작 (선택, 3분)

여유가 되면 P5.js Web Editor에서 새 스케치를 열고, 기본 구조를 잡습니다.

// 기말 프로젝트 프로토타입
// 프로젝트명: ___________
// 활용 기술: ___________

function setup() {
  createCanvas(600, 600);
  // TODO: 초기화 코드
}

function draw() {
  background(0);
  // TODO: 메인 비주얼
}

// TODO: 인터랙션

이 구조에 변수 선언, 클래스 정의, preload() 등을 추가해 갑니다. 오늘 다 완성할 필요 없습니다.


마무리

스케치를 꼭 저장합니다. Ctrl+S (Mac은 Cmd+S).

기말 프로젝트 일정 재확인

주차 할 일 비고
14주차 (오늘) 주제 구상 시작 키워드 + 스케치
15주차 작업 + 1:1 피드백 진행 중 상태로 가져오기
16주차 최종 제출 시연영상 + 소스코드
15주차 마음가짐

완성하지 않아도 됩니다. 진행 중인 상태로 가져오면 피드백을 받을 수 있습니다. 방향이 안 잡혔어도 괜찮습니다. 핵심은 다음 주에 무언가를 가져오는 것입니다. 빈 캔버스에 원 하나 그린 상태라도 좋습니다. 거기서부터 같이 발전시키면 됩니다.

14주차 학기 회고

1주차에서 ellipse(200, 200, 50, 50) 한 줄로 원을 그렸던 게 기억나나요?

이제 여러분은 소리를 분석하고, 데이터를 시각화하고, 수백 개의 파티클을 관리하고, 물리 법칙을 시뮬레이션하는 코드를 작성할 수 있습니다. 1주차의 그 원 하나에서 시작해서, 14주차의 사운드 리액티브 비주얼까지 온 것입니다.

기말 프로젝트에서 그 모든 것을 자신만의 작품으로 만들어 봅시다.

선택 사전 학습 영상


자기점검 기준

제출·채점하지 않는 수업 내 실습입니다. 아래는 스스로 완성도를 가늠하는 기준입니다.

Part A 기본 완료 기준 - 사운드가 정상 재생되고 getLevel() 또는 analyze()로 분석값을 받아옴 - 분석값이 map()으로 시각 요소(크기·색상·위치 중 1개 이상)에 연결됨 - 소리 변화에 따라 비주얼이 실시간으로 반응

Part A 확장 기준 - 잔상 효과, HSB 색상 순환, 다중 도형 등 2가지 이상 적용 - 인터랙션(마우스·키보드) 1개 이상 추가 - 13주차 파티클이나 이미지·웹캠과 결합

Part B 기본 완료 기준 - 주제와 방향이 키워드 3개로 정해짐 - 활용할 기술(2가지 이상)이 체크리스트에서 결정됨 - 대략적인 화면 구성이 종이에 스케치됨


선택 확장 과제

Part A를 빠르게 마친 뒤 더 해보고 싶다면 도전합니다.

  1. 파티클 + 사운드 결합 — 13주차 파티클 시스템을 가져와 amp.getLevel()에 따라 파티클 생성 개수를 조절 (2교시 심화 4 통합 사례 참고)
  2. 마이크 입력으로 전환loadSound() 대신 p5.AudioIn()을 써서 교실 소리에 반응 (1교시 핵심 5 참고)
  3. 이미지 + 사운드loadImage()로 이미지를 불러와 음량에 따라 크기·투명도 변화
  4. 이미지 파티클 — 13주차 파티클의 ellipseimage로 교체 (2교시 심화 2 회수)
  5. 원형 이퀄라이저 — 방향 B 완성 예제 참고, translate() + rotate() + cos()/sin() 활용

완성 예제 (방향별)

방향 A 완성 예제 — 음량 반응 동심원 + 잔상

let song;
let amp;

function preload() {
  song = loadSound('assets/music.mp3');
}

function setup() {
  createCanvas(400, 400);
  colorMode(HSB, 360, 100, 100, 100);
  amp = new p5.Amplitude();
}

function draw() {
  background(0, 0, 0, 10); // 잔상 효과

  let level = amp.getLevel();

  // 동심원 5개: 음량에 따라 크기 변화
  for (let i = 4; i >= 0; i--) {
    let size = map(level, 0, 0.5, 10, 350 - i * 60);
    size = max(size, 5); // 최소 크기 보장
    let hue = (frameCount + i * 40) % 360; // 색상 순환
    let alpha = map(i, 0, 4, 80, 20);

    fill(hue, 80, 100, alpha);
    noStroke();
    ellipse(width / 2, height / 2, size, size);
  }

  // 음량 표시 (디버그용 — 완성 시 삭제 가능)
  fill(0, 0, 100);
  noStroke();
  textSize(12);
  text('Level: ' + nf(level, 1, 3), 10, 20);
}

function mousePressed() {
  userStartAudio();
  // 재생 토글 — 반복 클릭 시 중복 재생 방지
  if (song.isPlaying()) {
    song.pause();
  } else {
    song.loop();
  }
}

방향 B 완성 예제 — 원형 이퀄라이저

let song;
let fft;

function preload() {
  song = loadSound('assets/music.mp3');
}

function setup() {
  createCanvas(400, 400);
  colorMode(HSB, 255);
  angleMode(DEGREES);
  fft = new p5.FFT(0.8, 128); // 128개 밴드
}

function draw() {
  background(0);
  translate(width / 2, height / 2);

  let spectrum = fft.analyze();

  for (let i = 0; i < spectrum.length; i++) {
    let angle = map(i, 0, spectrum.length, 0, 360);
    let r = map(spectrum[i], 0, 255, 50, 180); // 반지름

    let x = r * cos(angle);
    let y = r * sin(angle);

    fill(map(i, 0, spectrum.length, 0, 255), 255, 255);
    noStroke();
    ellipse(x, y, 5, 5);
  }

  // 가운데 원: 전체 음량 평균
  let level = 0;
  for (let i = 0; i < spectrum.length; i++) {
    level += spectrum[i];
  }
  level = level / spectrum.length;
  let centerSize = map(level, 0, 128, 20, 80);
  fill(255);
  ellipse(0, 0, centerSize, centerSize);
}

function mousePressed() {
  userStartAudio();
  // 재생 토글 — 반복 클릭 시 중복 재생 방지
  if (song.isPlaying()) {
    song.pause();
  } else {
    song.loop();
  }
}

방향 C 완성 예제 — 저음/중음/고음 분리 + 배경 반응

let song;
let fft;

function preload() {
  song = loadSound('assets/music.mp3');
}

function setup() {
  createCanvas(400, 400);
  fft = new p5.FFT();
}

function draw() {
  fft.analyze();

  let bass = fft.getEnergy("bass");
  let mid = fft.getEnergy("mid");
  let treble = fft.getEnergy("treble");

  // 배경색도 저음에 반응
  background(map(bass, 0, 255, 0, 30), 0, map(bass, 0, 255, 0, 20));

  // 저음 → 빨간 원들 (크기 + 투명도 변화)
  let bassSize = map(bass, 0, 255, 20, 250);
  for (let i = 0; i < 3; i++) {
    fill(255, 50, 50, map(i, 0, 2, 150, 30));
    noStroke();
    ellipse(width * 0.25, height / 2, bassSize + i * 30, bassSize + i * 30);
  }

  // 중음 → 초록 사각형 (회전)
  let midSize = map(mid, 0, 255, 10, 150);
  push();
  translate(width * 0.5, height / 2);
  rotate(frameCount * 0.02);
  fill(50, 255, 50, 180);
  noStroke();
  rectMode(CENTER);
  rect(0, 0, midSize, midSize);
  pop();

  // 고음 → 파란 삼각형 (깜빡임)
  let trebleSize = map(treble, 0, 255, 10, 120);
  let trebleAlpha = map(treble, 0, 255, 30, 255);
  fill(50, 80, 255, trebleAlpha);
  noStroke();
  triangle(
    width * 0.75, height / 2 - trebleSize / 2,
    width * 0.75 - trebleSize / 2, height / 2 + trebleSize / 2,
    width * 0.75 + trebleSize / 2, height / 2 + trebleSize / 2
  );

  // 범례
  fill(255);
  noStroke();
  textSize(12);
  text('BASS', width * 0.2, height - 20);
  text('MID', width * 0.47, height - 20);
  text('TREBLE', width * 0.68, height - 20);
}

function mousePressed() {
  userStartAudio();
  // 재생 토글 — 반복 클릭 시 중복 재생 방지
  if (song.isPlaying()) {
    song.pause();
  } else {
    song.loop();
  }
}