第一章 琴类作品
1.2吉他开发
吉他是音乐中的乐器王子, 又叫六弦琴拥有广阔的音域,最丰富的现场表现力,华丽而高难度的指弹技巧,又是伴奏的神器,现场演唱出优美而漂亮的音乐,适合任何场合任何环境的表现,自然深受大众的喜爱,自然成为最流行的乐器之首。
用手机六弦及六格模拟出三十六音效位置使手机的方寸之间能拥有二维空间音效的表现力。
1.2.1手机利用MediaPlayer发声
Android中很重要也最为复杂的媒体播放器---MediaPlayer.
Android的MediaPlayer包含了Audio和video的播放功能,在Android的界面上,Music和Video两个应用程序都是调用MediaPlayer实现的。
MediaPlayer在底层是基于OpenCore(PacketVideo)的库实现的,为了构建一个MediaPlayer程序,上层还包含了进程间通讯等内容,这种进程间通讯的基础是Android基本库中的Binder机制。
在java层MediaPlayer是以一个service作基础的,而这个service的主要内容又是通过MediaPlayer这个类调用的C++的媒体库实现的。代码中每个activity的处理逻辑是通过一个回调函数,调用一个将service提供的方法进一步封装的工具类。而我们使用时定义完直接调用就行了。
步骤1.新建一个空白工程,
步骤2.在res文件夹中新建raw文件夹,并将音效mp3文件拷入
步骤3. onCreate中添加两行代码,定义mMediaPlayer01,并指定raw.m11中的mp3音效文件,按下F11运行程序,就能播放声音。
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MediaPlayer mMediaPlayer01;
mMediaPlayer01 =MediaPlayer.create(MainActivity.this, R.raw.m11);mMediaPlayer01.start();
}
1.2.2吉他音效制作
方法一:通过录音,录下吉他弹奏的每个音阶,并通过GoldWave等软件生成1至2秒长的单音,同在效果菜单中调节音量及噪声等。
方法二:通过网络电脑找到下吉他弹奏的每个音阶的素材,如理想就直接使用,也可同时通过GoldWave等软件生成1至2秒长的单音,同在效果菜单中调节音量及噪声等。
方法三:手中有部分的吉他音阶的素材,也可通过GoldWave等软件通过音调调整生成新的吉他音阶,打开GoldWave中效果菜单,点击音调,选中“半音”,然后在右边框中填写需要变调的数值,按确定后就能生成新吉他音阶。同时可以用我们后面制作的手机听歌识曲等软件检查下生成的音阶是否正确。
1.2.3吉他画面的绘制
吉他弹奏与钢琴一个手指按下响一个音符不同,常常左手按弦,右手弹拨配合而成。例如捕获手指在屏幕上XY坐标为(50,50)就判断为按住第一弦的第一品格。
我在设计程序的时候考虑到,吉他六横六纵的的按键布局,仍利用计算按下屏幕位置来实现,可以少用控件,节省布局等因素,在手机方寸之间纵横位置都是有效利用,在手机上较有表现力。
1.定义一个全屏显示窗口,并强制横屏,setContentView(mAnimView);
public class MainActivity extends Activity {
MyView mAnimView = null;
MediaPlayer mMediaPlayer01;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
// 全屏显示窗口
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
//强制横屏
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
// 显示自定义的游戏View
2绘制MyView
a. 在mdpi中添加背景图片与按钮图片,注意分辨率。
b. 建立class MyViewextends SurfaceView并设置每30帧刷新一次屏幕。
c. 在draw()中显示背景图片与按钮图片,并在屏幕上显示文本drawText("X值:", 0, 20, mPaint);用于察看调试参数。
publicMyView(Context context) {
super(context);
/** 设置当前View拥有控制焦点 **/
this.setFocusable(true);
/** 设置当前View拥有触摸事件 **/
this.setFocusableInTouchMode(true);
mMediaPlayer01 = new MediaPlayer();
mMediaPlayer02 = new MediaPlayer();
mMediaPlayer03 = new MediaPlayer();
mMediaPlayer04 = new MediaPlayer();
mMediaPlayer05 = new MediaPlayer();
mMediaPlayer06 = new MediaPlayer();
/** 拿到SurfaceHolder对象 **/
mSurfaceHolder = this.getHolder();
/** 将mSurfaceHolder添加到Callback回调函数中 **/
mSurfaceHolder.addCallback(this);
/** 创建画布 **/
mCanvas = new Canvas();
/** 创建曲线画笔 **/
mPaint = new Paint();
mPaint.setColor(Color.WHITE);
mbitmapBall = BitmapFactory.decodeResource(this.getResources(), R.drawable.ball);
/**加载游戏背景**/
mbitmapBg = BitmapFactory.decodeResource(this.getResources(), R.drawable.bg);
}
1.2.4吉他弹奏的实现
1. 在touch事件中取得各手指的触屏位置,及手指全抬起时设为较大负值。例如捕获手指在屏幕上XY坐标为(50,50)就判断为按住第一弦的第一品格。然后手指全部不接触屏幕时XY坐标仍为为(50,50)。我们就要为手指全部不接触屏幕时指全部不接触屏幕时XY坐标设为一组不会操作的数值如,ry[i]=-1100;rx[i]=-1100;具体操作如下:
public boolean onTouchEvent(MotionEvent event)
{
p = event.getPointerCount();
for (i=0;i<p;i++) {
ry[i]=event.getX(i);
rx[i]=event.getY(i);
}
if (event.getAction()==MotionEvent.ACTION_UP)
{ for (i=0;i<5;i++){
ry[i]=-1100;rx[i]=-1100;} }
return true;
}
2. draw()中绘制按下的位置,并判断是否是按琴弦有没变化,将屏幕分成六横六纵的的按键布局,仍利用计算按下屏幕位置来实现设第一根弦每格设为11,21,31,41,51,61,第二根弦每格设为12,22,32,42,52,62,第三根弦每格设为13,23,33,43,53,63,按下比60大的,记数,设成拔弦,按下比60小的,记数,设成按弦。
例如下图:两手指按了31与36,是指左手按了第三弦第一品格,右手拔了第三弦。
程序中最难的算法就区别如判断按下的左手<60多个手指的位置,与右手>60拔弦发声的位置。具体如下:
private void Draw() {
int ip = 0;
/** 绘制游戏背景 **/
mCanvas.drawBitmap(mbitmapBg, 0, 0,mPaint);
for (i = 1;i < 7; i++) {
rk1[i] = 0;
rk3[i] = 0;
}
for (i = 0;i < p; i++) {
mCanvas.drawBitmap(mbitmapBall,ry[i] - 22,rx[i] - 22,mPaint);
//第一根弦每格设为11,21,31,41,51,61,
//第一根弦每格设为12,22,32,42,52,62,
rk[i] = (int) (ry[i] / 80) * 10 + 11 + (int) (rx[i] / 50);
for (ip = 1; ip < 7; ip++) {
//按下比60大的,记数,设成拔弦
if (rk[i] != 60 + ip) {
rk1[ip]++;
}
//按下比60小的,记数,设成按弦
if ((rk[i] % 10 == ip) & (rk[i] < 60)) {
if (rk3[ip] < (int) (rk[i] / 10)) {
rk3[ip] = (int) (rk[i] / 10);
}
}
}
}
if (rk1[1] ==p - 1) {
rk2[1]++;
} else {
rk2[1] = 0;
rk1[1] = 0;
}
if (rk2[1] == 1) {
mMediaPlayer01.release();
switch (rk3[1]) {
case 0:
mMediaPlayer01 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m23);
mMediaPlayer01.start();
break;
case 1:
mMediaPlayer01 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m24);
mMediaPlayer01.start();
break;
case 2:
mMediaPlayer01 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m17);
mMediaPlayer01.start();
break;
case 3:
mMediaPlayer01 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m25);
mMediaPlayer01.start();
break;
case 4:
mMediaPlayer01 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m22);
mMediaPlayer01.start();
break;
case 5:
mMediaPlayer01 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m26);
mMediaPlayer01.start();
break;
case 14:
mMediaPlayer01 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m23);
mMediaPlayer01.start();
break;
default:
break;// System.out.println("Other Condition");
}
}
if (rk1[2] ==p - 1) {
rk2[2]++;
} else {
rk2[2] = 0;
rk1[2] = 0;
}
if (rk2[2] == 1) {
mMediaPlayer02.release();
switch (rk3[2]) {
case 0:
mMediaPlayer02 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m17);
mMediaPlayer02.start();
break;
case 1:
mMediaPlayer02 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m21);
mMediaPlayer02.start();
break;
case 2:
mMediaPlayer02 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m13);
mMediaPlayer02.start();
break;
case 3:
mMediaPlayer02 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m22);
mMediaPlayer02.start();
break;
case 4:
mMediaPlayer02 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m225);
mMediaPlayer02.start();
break;
case 5:
mMediaPlayer02 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m23);
mMediaPlayer02.start();
break;
case 14:
mMediaPlayer02 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m23);
mMediaPlayer02.start();
break;
default:
break;// System.out.println("Other Condition");
}
}
if (rk1[3] ==p - 1) {
rk2[3]++;
} else {
rk2[3] = 0;
rk1[3] = 0;
}
if (rk2[3] == 1) {
mMediaPlayer03.release();
switch (rk3[3]) {
case 6:
mMediaPlayer03 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m13);
mMediaPlayer03.start();
break;
case 7:
mMediaPlayer03 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m14);
mMediaPlayer03.start();
break;
case 0:
mMediaPlayer03 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m15);
mMediaPlayer03.start();
break;
case 1:
mMediaPlayer03 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m155);
mMediaPlayer03.start();
break;
case 2:
mMediaPlayer03 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m16);
mMediaPlayer03.start();
break;
case 3:
mMediaPlayer03 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m14);
mMediaPlayer03.start();
break;
case 4:
mMediaPlayer03 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m17);
mMediaPlayer03.start();
break;
case 5:
mMediaPlayer03 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m23);
mMediaPlayer03.start();
break;
case 14:
mMediaPlayer03 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m23);
mMediaPlayer03.start();
break;
default:
break;// System.out.println("Other Condition");
}
}
if (rk1[4] ==p - 1) {
rk2[4]++;
} else {
rk2[4] = 0;
rk1[4] = 0;
}
最后给出完整的代码
第一章 琴类作品
1.2吉他开发
吉他是音乐中的乐器王子, 又叫六弦琴拥有广阔的音域,最丰富的现场表现力,华丽而高难度的指弹技巧,又是伴奏的神器,现场演唱出优美而漂亮的音乐,适合任何场合任何环境的表现,自然深受大众的喜爱,自然成为最流行的乐器之首。
用手机六弦及六格模拟出三十六音效位置使手机的方寸之间能拥有二维空间音效的表现力。
1.2.1手机利用MediaPlayer发声
Android中很重要也最为复杂的媒体播放器---MediaPlayer.
Android的MediaPlayer包含了Audio和video的播放功能,在Android的界面上,Music和Video两个应用程序都是调用MediaPlayer实现的。
MediaPlayer在底层是基于OpenCore(PacketVideo)的库实现的,为了构建一个MediaPlayer程序,上层还包含了进程间通讯等内容,这种进程间通讯的基础是Android基本库中的Binder机制。
在java层MediaPlayer是以一个service作基础的,而这个service的主要内容又是通过MediaPlayer这个类调用的C++的媒体库实现的。代码中每个activity的处理逻辑是通过一个回调函数,调用一个将service提供的方法进一步封装的工具类。而我们使用时定义完直接调用就行了。
步骤1.新建一个空白工程,
步骤2.在res文件夹中新建raw文件夹,并将音效mp3文件拷入
步骤3. onCreate中添加两行代码,定义mMediaPlayer01,并指定raw.m11中的mp3音效文件,按下F11运行程序,就能播放声音。
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MediaPlayer mMediaPlayer01;
mMediaPlayer01 =MediaPlayer.create(MainActivity.this, R.raw.m11);mMediaPlayer01.start();
}
1.2.2吉他音效制作
方法一:通过录音,录下吉他弹奏的每个音阶,并通过GoldWave等软件生成1至2秒长的单音,同在效果菜单中调节音量及噪声等。
方法二:通过网络电脑找到下吉他弹奏的每个音阶的素材,如理想就直接使用,也可同时通过GoldWave等软件生成1至2秒长的单音,同在效果菜单中调节音量及噪声等。
方法三:手中有部分的吉他音阶的素材,也可通过GoldWave等软件通过音调调整生成新的吉他音阶,打开GoldWave中效果菜单,点击音调,选中“半音”,然后在右边框中填写需要变调的数值,按确定后就能生成新吉他音阶。同时可以用我们后面制作的手机听歌识曲等软件检查下生成的音阶是否正确。
1.2.3吉他画面的绘制
吉他弹奏与钢琴一个手指按下响一个音符不同,常常左手按弦,右手弹拨配合而成。例如捕获手指在屏幕上XY坐标为(50,50)就判断为按住第一弦的第一品格。
我在设计程序的时候考虑到,吉他六横六纵的的按键布局,仍利用计算按下屏幕位置来实现,可以少用控件,节省布局等因素,在手机方寸之间纵横位置都是有效利用,在手机上较有表现力。
1.定义一个全屏显示窗口,并强制横屏,setContentView(mAnimView);
public class MainActivity extends Activity {
MyView mAnimView = null;
MediaPlayer mMediaPlayer01;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
// 全屏显示窗口
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
//强制横屏
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
// 显示自定义的游戏View
2绘制MyView
a. 在mdpi中添加背景图片与按钮图片,注意分辨率。
b. 建立class MyViewextends SurfaceView并设置每30帧刷新一次屏幕。
c. 在draw()中显示背景图片与按钮图片,并在屏幕上显示文本drawText("X值:", 0, 20, mPaint);用于察看调试参数。
publicMyView(Context context) {
super(context);
/** 设置当前View拥有控制焦点 **/
this.setFocusable(true);
/** 设置当前View拥有触摸事件 **/
this.setFocusableInTouchMode(true);
mMediaPlayer01 = new MediaPlayer();
mMediaPlayer02 = new MediaPlayer();
mMediaPlayer03 = new MediaPlayer();
mMediaPlayer04 = new MediaPlayer();
mMediaPlayer05 = new MediaPlayer();
mMediaPlayer06 = new MediaPlayer();
/** 拿到SurfaceHolder对象 **/
mSurfaceHolder = this.getHolder();
/** 将mSurfaceHolder添加到Callback回调函数中 **/
mSurfaceHolder.addCallback(this);
/** 创建画布 **/
mCanvas = new Canvas();
/** 创建曲线画笔 **/
mPaint = new Paint();
mPaint.setColor(Color.WHITE);
mbitmapBall = BitmapFactory.decodeResource(this.getResources(), R.drawable.ball);
/**加载游戏背景**/
mbitmapBg = BitmapFactory.decodeResource(this.getResources(), R.drawable.bg);
}
1.2.4吉他弹奏的实现
1. 在touch事件中取得各手指的触屏位置,及手指全抬起时设为较大负值。例如捕获手指在屏幕上XY坐标为(50,50)就判断为按住第一弦的第一品格。然后手指全部不接触屏幕时XY坐标仍为为(50,50)。我们就要为手指全部不接触屏幕时指全部不接触屏幕时XY坐标设为一组不会操作的数值如,ry[i]=-1100;rx[i]=-1100;具体操作如下:
public boolean onTouchEvent(MotionEvent event)
{
p = event.getPointerCount();
for (i=0;i<p;i++) {
ry[i]=event.getX(i);
rx[i]=event.getY(i);
}
if (event.getAction()==MotionEvent.ACTION_UP)
{ for (i=0;i<5;i++){
ry[i]=-1100;rx[i]=-1100;} }
return true;
}
2. draw()中绘制按下的位置,并判断是否是按琴弦有没变化,将屏幕分成六横六纵的的按键布局,仍利用计算按下屏幕位置来实现设第一根弦每格设为11,21,31,41,51,61,第二根弦每格设为12,22,32,42,52,62,第三根弦每格设为13,23,33,43,53,63,按下比60大的,记数,设成拔弦,按下比60小的,记数,设成按弦。
例如下图:两手指按了31与36,是指左手按了第三弦第一品格,右手拔了第三弦。
程序中最难的算法就区别如判断按下的左手<60多个手指的位置,与右手>60拔弦发声的位置。具体如下:
private void Draw() {
int ip = 0;
/** 绘制游戏背景 **/
mCanvas.drawBitmap(mbitmapBg, 0, 0,mPaint);
for (i = 1;i < 7; i++) {
rk1[i] = 0;
rk3[i] = 0;
}
for (i = 0;i < p; i++) {
mCanvas.drawBitmap(mbitmapBall,ry[i] - 22,rx[i] - 22,mPaint);
//第一根弦每格设为11,21,31,41,51,61,
//第一根弦每格设为12,22,32,42,52,62,
rk[i] = (int) (ry[i] / 80) * 10 + 11 + (int) (rx[i] / 50);
for (ip = 1; ip < 7; ip++) {
//按下比60大的,记数,设成拔弦
if (rk[i] != 60 + ip) {
rk1[ip]++;
}
//按下比60小的,记数,设成按弦
if ((rk[i] % 10 == ip) & (rk[i] < 60)) {
if (rk3[ip] < (int) (rk[i] / 10)) {
rk3[ip] = (int) (rk[i] / 10);
}
}
}
}
if (rk1[1] ==p - 1) {
rk2[1]++;
} else {
rk2[1] = 0;
rk1[1] = 0;
}
if (rk2[1] == 1) {
mMediaPlayer01.release();
switch (rk3[1]) {
case 0:
mMediaPlayer01 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m23);
mMediaPlayer01.start();
break;
case 1:
mMediaPlayer01 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m24);
mMediaPlayer01.start();
break;
case 2:
mMediaPlayer01 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m17);
mMediaPlayer01.start();
break;
case 3:
mMediaPlayer01 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m25);
mMediaPlayer01.start();
break;
case 4:
mMediaPlayer01 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m22);
mMediaPlayer01.start();
break;
case 5:
mMediaPlayer01 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m26);
mMediaPlayer01.start();
break;
case 14:
mMediaPlayer01 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m23);
mMediaPlayer01.start();
break;
default:
break;// System.out.println("Other Condition");
}
}
if (rk1[2] ==p - 1) {
rk2[2]++;
} else {
rk2[2] = 0;
rk1[2] = 0;
}
if (rk2[2] == 1) {
mMediaPlayer02.release();
switch (rk3[2]) {
case 0:
mMediaPlayer02 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m17);
mMediaPlayer02.start();
break;
case 1:
mMediaPlayer02 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m21);
mMediaPlayer02.start();
break;
case 2:
mMediaPlayer02 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m13);
mMediaPlayer02.start();
break;
case 3:
mMediaPlayer02 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m22);
mMediaPlayer02.start();
break;
case 4:
mMediaPlayer02 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m225);
mMediaPlayer02.start();
break;
case 5:
mMediaPlayer02 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m23);
mMediaPlayer02.start();
break;
case 14:
mMediaPlayer02 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m23);
mMediaPlayer02.start();
break;
default:
break;// System.out.println("Other Condition");
}
}
if (rk1[3] ==p - 1) {
rk2[3]++;
} else {
rk2[3] = 0;
rk1[3] = 0;
}
if (rk2[3] == 1) {
mMediaPlayer03.release();
switch (rk3[3]) {
case 6:
mMediaPlayer03 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m13);
mMediaPlayer03.start();
break;
case 7:
mMediaPlayer03 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m14);
mMediaPlayer03.start();
break;
case 0:
mMediaPlayer03 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m15);
mMediaPlayer03.start();
break;
case 1:
mMediaPlayer03 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m155);
mMediaPlayer03.start();
break;
case 2:
mMediaPlayer03 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m16);
mMediaPlayer03.start();
break;
case 3:
mMediaPlayer03 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m14);
mMediaPlayer03.start();
break;
case 4:
mMediaPlayer03 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m17);
mMediaPlayer03.start();
break;
case 5:
mMediaPlayer03 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m23);
mMediaPlayer03.start();
break;
case 14:
mMediaPlayer03 = MediaPlayer.create(
SurfaceViewAcitvity.this, R.raw.m23);
mMediaPlayer03.start();
break;
default:
break;// System.out.println("Other Condition");
}
}
if (rk1[4] ==p - 1) {
rk2[4]++;
} else {
rk2[4] = 0;
rk1[4] = 0;
}
最后给出完整的代码
视频演示:http://blog.sina.com.cn/s/blog_6281e4430102wmyg.html