はじめに
今回も、変なプログラムを作ったので紹介していきたいと思います。
砂嵐を作成し、その砂嵐をスマホにかざして音楽を再生するというものです。
実際に遊んでみよう!
仕組み
仕組みはとても単純で、
- 曲の一定区間の波形を画像のRGB値に埋め込む
- Webカメラを使って画像(映像)を読み込む
- 映像を解析して、波形に戻す
これを、区間を随時切り替えて実行します。
コード
※適当な正方形のPictureBoxを配置してください。
半分くらい、高校生の頃に書いたものを持ってきたので、結構汚いです。
Windowsフォームアプリケーション(NET.Framework)
Form1.cs
using System;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace Sample
{
public partial class Form1 : Form
{
const int fps = 15; //大きくすると粗い画像が高速に描画。小さいとその逆。
string fn;
int sample;
int now = 0;
short[] val;
int fileSize;
int wh = 1;
int fr = 0;
int ch = 0;
Stopwatch sw;
public Form1()
{
InitializeComponent();
BS();
}
public void BS()
{
//ファイルを開く
using (var ofDialog = new OpenFileDialog
{
InitialDirectory = @"C:",
Title = "音楽を選択"
})
{
if (ofDialog.ShowDialog() == DialogResult.OK)
{
fn = ofDialog.FileName;
if (Path.GetExtension(fn) == ".wav")
{
Read();
sw = new Stopwatch();
sw.Start();
Itv();
}
}
}
}
public void Itv()
{
//無限ループ用
Draw();
Itv2();
}
public async void Itv2()
{
//無限ループ用
await Task.Delay(1);
fr++;
this.Text = "" + fr;
Itv();
}
public void Draw()
{
now = (int)(sw.ElapsedMilliseconds / 1000.0 * 44100 * ch);
if (pictureBox1.Image != null)
{
pictureBox1.Image.Dispose();
}
Rectangle rect = new Rectangle(0, 0, wh, wh);
//適当なBitmapを用意して書き込み
Bitmap tmp = new Bitmap(wh, wh);
BitmapData source_data = tmp.LockBits(rect, ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
byte[] imgdata = new byte[wh * wh * 4];
try
{
for (int j = 0; j < imgdata.Length; j += 4)
{
for (int i = 0; i < 3; i++)
{
if (now >= val.Length)
{
now = 0;
sw.Restart();
}
imgdata[j + i] = (byte)(128 + val[now] / 256.0);
now++;
}
//透過度はすべて255
imgdata[j + 3] = 255;
}
}
finally
{
tmp.UnlockBits(source_data);
}
System.Runtime.InteropServices.Marshal.Copy(imgdata, 0, source_data.Scan0, imgdata.Length);
Bitmap canvas = new Bitmap(pictureBox1.Width, pictureBox1.Height);
using (Graphics g = Graphics.FromImage(canvas))
{
//拡大した時に画像はぼかさない
g.InterpolationMode = InterpolationMode.NearestNeighbor;
g.DrawImage(tmp, 0, 0, pictureBox1.Width, pictureBox1.Height);
}
pictureBox1.Image = canvas;
}
public void Read()
{
//wavファイルを開く
int data = 0;
byte[] buf;
int bit = 0;
using (var fs = new FileStream(
fn, FileMode.Open, FileAccess.Read))
{
fileSize = (int)fs.Length;
buf = new byte[fileSize];
fs.Read(buf, 0, fileSize);
}
//ヘッダーとデータ部分け
for (int i = 0; i < buf.Length; i++)
{
if (i < buf.Length - 4)
{
if (buf[i] == 100 && buf[i + 1] == 97 && buf[i + 2] == 116 && buf[i + 3] == 97)
{
data = i + 8;
i = buf.Length;
}
}
}
//いろいろ読み取り
for (int i = 23; i > 21; i--)
{
ch += (int)(buf[i] * Math.Pow(256, i - 22));
}
for (int i = 27; i > 23; i--)
{
sample += (int)(buf[i] * Math.Pow(256, i - 24));
}
for (int i = 35; i > 33; i--)
{
bit += (int)(buf[i] * Math.Pow(256, i - 34));
}
//16bitの波形を読み取り
val = BtoS(buf, data);
//砂嵐映像の解像度の1辺を計算
wh = (int)Math.Round(Math.Sqrt(1.0 * ch * sample / fps / 3));
}
public short[] BtoS(byte[] x, int start)
{
short[] rt = new short[(x.Length - start) / 2];
for (int i = 0; i < x.Length - start; i += 2)
{
if (x[i + 1] >= 128)
{
rt[i / 2] = (short)(x[i + 1 + start] * 256 - 65536 + x[i + start]);
}
else
{
rt[i / 2] = (short)(x[i + 1 + start] * 256 + x[i + start]);
}
}
return rt;
}
}
}
HTML
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>てすと</title>
</head>
<body>
<div id="main" style="display: none;">
<video id="video" src="" width="600" height="600" playsinline></video>
<canvas id="canvas" width="1" height="1" style="visibility: hidden;"></canvas>
<br><br><h2>設定</h2>
サンプリングレート:<input id="sp" type="number" value="44100" min="8000" max="48000"><br>
FPS :<input id="fps" type="number" value="15" min="2" max="60"><br>
<button onclick="opt()">更新</button>
</div>
<div id="sub">
<h2>画面クリックで開く</h2>
</div>
<script src="main.js"></script>
</body>
</html>
JavaScript
main.js
let wh = 0;
let fps = 0;
let sample = 0;
let start = false;
let tm = 0;
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');
const ctx = new AudioContext();
const gain = ctx.createGain();
gain.value = 0.5;
gain.connect(ctx.destination);
window.addEventListener('load', function(){
opt();
start = true;
});
//中身は引用
function init(){
let stream = null;
const constraints = {
audio: false,
video: {
width: wh,
height: wh,
facingMode: { exact: "environment" },
},
}
async function startCamera(constraints) {
try {
stream = await navigator.mediaDevices.getUserMedia(constraints);
const video = document.getElementById('video');
video.srcObject = stream;
video.onloadedmetadata = () => {
video.play();
};
} catch (err) {
console.error(err);
}
}
startCamera(constraints);
}
function opt(){
//inputを読み取り
sample = Number(document.getElementById("sp").value);
fps = Number(document.getElementById("fps").value);
//砂嵐映像の解像度の1辺を計算
wh = Math.round(Math.sqrt(sample * 2 / fps / 3));
document.getElementById("canvas").width = wh;
document.getElementById("canvas").height = wh;
init();
}
window.onclick = function(){
//ダブルバッファリング
makewave();
makewave();
//画面の切りかえ
document.getElementById("main").style.display = "block";
document.getElementById("sub").style.display = "none";
}
function makewave(){
if(start){
//カメラから画像を取得
const video = document.getElementById('video');
context.drawImage(video, 0, 0, canvas.width, canvas.height);
//配列に直す
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
//波形を生成
const audioBuffer = ctx.createBuffer(2,Math.floor(wh * wh * 3 / 2),sample);
const wave = audioBuffer.getChannelData(0);
const wave2 = audioBuffer.getChannelData(1);
let j = 0;
for (let i = 0; i < data.length; i++) {
if(i % 4 != 3){
if(j % 2 == 0){
wave[Math.floor(j / 2)] = data[i] / 128 - 1;
}else{
wave2[Math.floor(j / 2)] = data[i] / 128 - 1;
}
j++;
}
}
//いろいろ設定
const source = ctx.createBufferSource();
source.buffer = audioBuffer;
source.onended = makewave;
source.connect(gain);
source.start(tm);
tm += 1.0 / fps;
}
}
一応...
変数fpsについて
変数fpsは、作っている間に仕様を変えたため、意味が変わっています。
変数whについて
C#とJSにある、砂嵐映像の解像度の1辺whは、奇数だと2chの波形をうまく収納できないのですが、音質に影響がなかったため、そのままにしてあります。
どうしても気になるなら、計算した後の行に、
if(wh % 2 == 1) wh++;
のように描いてあげると良いです。
作ったプログラムを使ってみよう!
- C#で作ったプログラムを開く
- 任意の2ch 16bitのwavファイルを開く
- ここで映像が流れるはず
- HTMLファイルを開く
- カメラを許可する(この後雑音注意!!)
- (入力欄に設定したFPS、入れたwavのサンプリングレートを入れて「更新」を押す)
- C#で流れている映像をスマホでピッタリかざす
- 雑音の中から曲が聞こえる
まとめ
今回は、砂嵐をスマホにかざして音楽を再生するプログラムの作成について紹介してみました。
初心者で、おもしろいなと感じて、プログラミングに興味を持っていただけると幸いです。