七夕了,用代碼給心愛的人寫一首曲子吧

web頁面能發出聲音的方法有兩種,一種是autiovideo這些標籤,另一種就是音頻上下文AudioContext。接下來咱們看一下如何使用AudioContext,寫簡易鋼琴和曲子。而後趕在七夕以前,給心愛的人做一首曲子吧javascript

1. AudioContext如何發出聲音

Mdn上面有具體介紹,咱們這裏只用下面幾個css

// 建立音頻上下文
      const audioCtx = new AudioContext();
      // 建立音調控制對象
      const oscillator = audioCtx.createOscillator();
      // 建立音量控制對象
      const gainNode = audioCtx.createGain();
      // 音調音量關聯
      oscillator.connect(gainNode);
      // 音量和設備關聯
      gainNode.connect(audioCtx.destination);
      // 音調類型指定爲正弦波。sin好聽一些
      oscillator.type = "sine";
      // 設置音調頻率(做曲的關鍵)
      oscillator.frequency.value = 400;
      // 先把當前音量設爲0
      gainNode.gain.setValueAtTime(0, audioCtx.currentTime);
      // 0.01秒時間內音量從0到1線性變化,忽然變化的話很生硬
      gainNode.gain.linearRampToValueAtTime(
        1,
        audioCtx.currentTime + 0.01
      );
      // 聲音開始
      oscillator.start(audioCtx.currentTime);
複製代碼

不,這還沒完,直接copy這段代碼放你js文件裏面是沒用的前端

這裏還須要一步:地址欄中輸入chrome://flags/#autoplay-policy,把autoplay-policy改爲圖中所示java

ok,如今代碼能夠發出聲音了,可是不會停下來,咱們須要把音頻停下來:程序員

oscillator.stop(audioCtx.currentTime + 1);
複製代碼

2. 簡譜怎麼來

如今咱們知道怎麼發出聲音了,接下來是如何發出想要的聲音,便是如何知道哆來咪發唆這些音所對應的頻率是多少。web

搜索關鍵詞:簡譜頻率、曲譜頻率。很快,就能夠找到映射表格chrome

咱們要作的就是把oscillator.frequency.value = 400;這裏的數字改爲頻率便可發出對應的聲音。不妨先試一下數組

3. 根據簡譜映射表輸出對應的音頻

上面那個表抄了一份,具體以下,表明着低中高的哆來咪發唆啦希數據結構

點擊查看簡譜數組
[[261.63, 293.67, 329.63, 349.23, 391.99, 440, 493.88], [523.25, 587.33, 659.26, 698.46, 783.99, 880, 987.77], [1046.5, 1174.66, 1318.51, 1396.92, 1567.98, 1760, 1975.52]]
複製代碼

只要把它和點擊事件聯繫起來,就能夠作到一個小鋼琴了。先使用js渲染出每個按鍵app

// 簡譜映射
    const VOICE_MAP = {
      0: [261.63, 293.67, 329.63, 349.23, 391.99, 440, 493.88],
      1: [523.25, 587.33, 659.26, 698.46, 783.99, 880, 987.77],
      2: [1046.5, 1174.66, 1318.51, 1396.92, 1567.98, 1760, 1975.52]
    };
    function renderBtns(level) {
      let i = 0;
      let res = "";
      while (i < 7) {
        res += `<span class="btn level${level}" data-index=${i}>${i + 1}</span>`; // 用data-屬性輔助
        i++;
      }
      const container = document.createElement("section");
      container.className = `container${level}`;
      // ------------------------
      // 等下這裏會加一些事件綁定
      // ------------------------
      container.innerHTML += res;
      document.body.appendChild(container);
    }

    // 渲染節點
    renderBtns(0);
    renderBtns(1);
    renderBtns(2);

複製代碼
點擊查看樣式
.btn {
        cursor: pointer;
        display: inline-block;
        width: 100px;
        height: 30px;
        line-height: 30px;
        user-select: none;
        text-align: center;
        border: 1px #a12d21 solid;
        margin: 2px;
      }

      .level0::after {
        content: ".";
        position: relative;
        top: 4px;
        left: -7px;
      }

      .level2::before {
        content: ".";
        position: relative;
        top: -16px;
        left: 7px;
      }

複製代碼

最終效果以下

綁定事件

renderBtns方法加上事件綁定,移動端只需手動換成touch系列事件。

// 音頻開始
    function handleStart({ target }, level) {
      const {
        dataset: { index }
      } = target;
      if (index !== undefined) {
        console.log(index, "start");
        playAudio.call(target, index, level); // 後面加上playAudio的實現
      }
    }

    // 中止音頻
    function handleStop({ target }) {
      const {
        dataset: { index }
      } = target;
      if (index !== undefined) {
        console.log(index, "stop");
        stopAudio.call(target); // 後面加上stopAudio的實現
      }
    }

    function renderBtns(level) {
      let i = 0;
      let res = "";
      while (i < 7) {
        res += `<span class="btn level${level}" data-index=${i}>${i + 1}</span>`;
        i++;
      }
      const container = document.createElement("section");
      container.className = `container${level}`;
      // 傳入e和level,level指的是低中高音
      const particalStart = e => handleStart(e, level);
      container.addEventListener("mousedown", e => {
        particalStart(e);
        container.addEventListener("mouseout", handleStop);
      });
      container.addEventListener("mouseup", handleStop);
      container.innerHTML += res;
      document.body.appendChild(container);
    }
複製代碼

爲何使用call?後面stopAudioplayAudio用了this,這樣子就能夠作到dom節點與事件和音頻一對一綁定了

4. 播放音頻和中止音頻

// 音頻上下文
    const audioCtx = new AudioContext();

    function playAudio(index, level) {
      // 若是以前正在播,那就清掉以前的音頻
      this.gainNode &&
        this.gainNode.gain.setValueAtTime(0, audioCtx.currentTime);
      this.oscillator && this.oscillator.stop(audioCtx.currentTime + 1);
      // 建立音調控制對象
      this.oscillator = audioCtx.createOscillator();
      // 建立音量控制對象
      this.gainNode = audioCtx.createGain();
      // 音調音量關聯
      this.oscillator.connect(this.gainNode);
      // 音量和設備關聯
      this.gainNode.connect(audioCtx.destination);
      // 音調類型指定爲正弦波。sin好聽一些
      this.oscillator.type = "sine";
      // 設置音調頻率
      this.oscillator.frequency.value = VOICE_MAP[level][index]; // 讀取相應的簡譜頻率
      // 先把當前音量設爲0
      this.gainNode.gain.setValueAtTime(0, audioCtx.currentTime);
      // 0.01秒時間內音量從剛剛的0變成1,線性變化
      this.gainNode.gain.linearRampToValueAtTime(
        1,
        audioCtx.currentTime + 0.01
      );
      // 聲音開始
      this.oscillator.start(audioCtx.currentTime);
    }

    function stopAudio() {
      this.gainNode &&
        this.gainNode.gain.exponentialRampToValueAtTime(
          0.001,
          audioCtx.currentTime + 0.8
        );
      // 0.8秒內中止聲音
      this.oscillator && this.oscillator.stop(audioCtx.currentTime + 0.8);
      this.oscillator = this.gainNode = null;
    }
複製代碼
以上所有js代碼
const VOICE_MAP = {
      0: [261.63, 293.67, 329.63, 349.23, 391.99, 440, 493.88],
      1: [523.25, 587.33, 659.26, 698.46, 783.99, 880, 987.77],
      2: [1046.5, 1174.66, 1318.51, 1396.92, 1567.98, 1760, 1975.52]
    };

    function handleStart({ target }, level) {
      const {
        dataset: { index }
      } = target;
      if (index !== undefined) {
        console.log(index, "start");
        playAudio.call(target, index, level);
      }
    }

    function handleStop({ target }) {
      const {
        dataset: { index }
      } = target;
      if (index !== undefined) {
        console.log(index, "stop");
        stopAudio.call(target);
      }
    }

    function renderBtns(level) {
      let i = 0;
      let res = "";
      while (i < 7) {
        res += `<span class="btn level${level}" data-index=${i}>${i + 1}</span>`;
        i++;
      }
      const container = document.createElement("section");
      container.className = `container${level}`;
      const particalStart = e => handleStart(e, level);
      container.addEventListener("mousedown", e => {
        particalStart(e);
        container.addEventListener("mouseout", handleStop);
      });
      container.addEventListener("mouseup", handleStop);
      container.innerHTML += res;
      document.body.appendChild(container);
    }

    renderBtns(0);
    renderBtns(1);
    renderBtns(2);

    // 音頻上下文
    const audioCtx = new AudioContext();

    function playAudio(index, level) {
      // 若是以前正在播,那就清掉以前的音頻
      this.gainNode &&
        this.gainNode.gain.setValueAtTime(0, audioCtx.currentTime);
      this.oscillator && this.oscillator.stop(audioCtx.currentTime + 1);
      // 建立音調控制對象
      this.oscillator = audioCtx.createOscillator();
      // 建立音量控制對象
      this.gainNode = audioCtx.createGain();
      // 音調音量關聯
      this.oscillator.connect(this.gainNode);
      // 音量和設備關聯
      this.gainNode.connect(audioCtx.destination);
      // 音調類型指定爲正弦波。sin好聽一些
      this.oscillator.type = "sine";
      // 設置音調頻率
      this.oscillator.frequency.value = VOICE_MAP[level][index];
      // 先把當前音量設爲0
      this.gainNode.gain.setValueAtTime(0, audioCtx.currentTime);
      // 0.01秒時間內音量從剛剛的0變成1,線性變化
      this.gainNode.gain.linearRampToValueAtTime(
        1,
        audioCtx.currentTime + 0.01
      );
      // 聲音開始
      this.oscillator.start(audioCtx.currentTime);
    }

    function stopAudio() {
      // 0.8秒後中止聲音
      this.gainNode &&
        this.gainNode.gain.exponentialRampToValueAtTime(
          0.001,
          audioCtx.currentTime + 0.8
        );
      this.oscillator && this.oscillator.stop(audioCtx.currentTime + 0.8);
      this.oscillator = this.gainNode = null;
    }
複製代碼

如今,就是一個小鋼琴了,能夠隨便彈奏本身的歌曲。那麼,問題來了,想彈一首歌,該怎麼按鍵?搜索:xxx簡譜,對着彈便可

5. 自動播放

固然,對於程序員確定想辦法搞自動的。咱們已經知道怎麼輸出想要的音頻了,接下來就是如何將真正的歌曲以js的數據結構保存,並使用AudioContext API輸出的事情了。個人實現是直接複用上面的事件綁定代碼,使用腳本觸發原生事件。固然還有不少其餘方法實現。

// 先來一個sleep,確定須要使用延遲的
    function sleep(delay = 80) {
      return new Promise(r =>
        setTimeout(() => {
          r();
        }, delay)
      );
    }
    /** * @params arr 歌譜數組 * @example * { level: 0, index: 0 } 低音的哆 * { stop: true } 下一個循環什麼都沒有 * { delay: true } 下一個循環什麼都不作 */
    async function diyPlay(arr) {
      let cursor = 0;
      const a = [...arr];
      const containers = document.querySelectorAll("section");
      let ele;
      // 一個個遍歷歌曲數組
      while (arr.length) {
        // 先延遲一下,就能夠避免上一個音戛然而止了
        await sleep(300);
        const current = a.shift();
        // 留一個delay接口,便是延長一下上一個音
        if (current && current.delay) {
          continue;
        }
        // 下一個按鍵,停下以前的音
        if (ele) {
          // 手動用js觸發原生事件中止音頻
          const evPre = document.createEvent("MouseEvents");
          evPre.initMouseEvent("mouseout", true, true, window);
          ele.dispatchEvent(evPre);
        }
        if (!arr.length || !current) {
          return;
        }
        // 
        if (current.stop) {
          continue;
        }
        await sleep(50); // 加一點延遲使得多個連續相同的音天然一些
        const ev = document.createEvent("MouseEvents");
        ele = containers[current.level].children[current.index - 1];
        // 手動用js觸發原生事件開始音頻
        if (ele) {
          ev.initMouseEvent("mousedown", true, true, window);
          ele.dispatchEvent(ev);
        }
      }
    }
複製代碼

如何將簡譜轉化爲可用數據結構

好比下面簡譜:

轉成上文的diyPlay函數所須要的數據結構

[
      { level: 1, index: 3 }, 
      { level: 1, index: 3 },
      { level: 1, index: 5 },
      { level: 1, index: 5 },
      { level: 2, index: 1 },
      { level: 2, index: 1 },
      { level: 1, index: 7 },
      { delay: true }, // 7後面延遲一下
      { level: 1, index: 7 }, 
      { level: 1, index: 6 },
      { level: 1, index: 3 },
      { level: 1, index: 6 },
      { delay: true }, // 6和6是連的,delay一下
      { stop: true }, // 一句唱完了,停一下
]
複製代碼

按照規律,咱們隨便搜一首歌抄一下就能夠用代碼輸出了

因而,先上一首抖音歌曲吧:

// 《地鐵等待》
    diyPlay([{"level":2,"index":3},{"level":2,"index":4},{"level":2,"index":5},{"level":2,"index":5},{"level":2,"index":5},{"level":2,"index":3},{"level":2,"index":2},{"level":2,"index":2},{"level":2,"index":5},{"delay":true},{"stop":true},{"stop":true},{"level":2,"index":1},{"level":2,"index":1},{"level":1,"index":7},{"level":2,"index":3},{"level":2,"index":3},{"level":2,"index":1},{"level":1,"index":7},{"delay":true},{"level":2,"index":3},{"delay":true},{"stop":true},{"stop":true},{"level":1,"index":6},{"level":2,"index":1},{"level":2,"index":1},{"level":1,"index":6},{"level":1,"index":5},{"level":1,"index":5},{"level":2,"index":1},{"delay":true},{"stop":true},{"stop":true},{"level":2,"index":4},{"level":2,"index":3},{"level":2,"index":1},{"level":2,"index":2},{"level":2,"index":3},{"delay":true},{"level":2,"index":2},{"stop":true},{"stop":true},{"level":2,"index":5},{"level":2,"index":5},{"level":2,"index":5},{"level":2,"index":3},{"level":2,"index":2},{"delay":true},{"level":2,"index":5},{"level":2,"index":5},{"level":2,"index":2},{"stop":true},{"stop":true},{"level":2,"index":1},{"level":2,"index":3},{"level":2,"index":3},{"level":2,"index":1},{"level":1,"index":7},{"delay":true},{"level":2,"index":3},{"stop":true},{"stop":true},{"level":1,"index":6},{"level":2,"index":1},{"level":2,"index":1},{"level":2,"index":6},{"level":1,"index":5},{"delay":true},{"level":1,"index":6},{"level":2,"index":1},{"level":2,"index":2},{"level":2,"index":3},{"stop":true},{"stop":true},{"level":2,"index":4},{"level":2,"index":3},{"level":2,"index":1},{"level":2,"index":2},{"delay":true},{"delay":true},{"stop":true}]);
複製代碼
《小幸運簡譜代碼》
// 抄得匆匆忙忙,後面有一些不許確的
diyPlay([
      { level: 1, index: 3 }, // 我聽見雨落在青青草地
      { level: 1, index: 3 },
      { level: 1, index: 5 },
      { level: 1, index: 5 },
      { level: 2, index: 1 },
      { level: 2, index: 1 },
      { level: 1, index: 7 },
      { delay: true },
      { level: 1, index: 7 }, 
      { level: 1, index: 6 },
      { level: 1, index: 3 },
      { level: 1, index: 6 },
      { level: 1, index: 6 },
      { delay: true },
      { stop: true },
      { level: 1, index: 6 }, // 我聽見遠方下課鐘聲響起
      { level: 1, index: 6 },
      { level: 1, index: 7 },
      { level: 1, index: 7 },
      { level: 2, index: 3 },
      { level: 2, index: 3 },
      { level: 1, index: 7 },
      { delay: true },
      { level: 1, index: 5 }, 
      { level: 1, index: 3 },
      { level: 1, index: 5 },
      { delay: true },

      { level: 1, index: 3 },// 但是我沒有聽見你的聲音
      { level: 1, index: 3 },
      { level: 1, index: 5 },
      { level: 1, index: 5 },
      { level: 2, index: 1 },
      { delay: true },

      { level: 1, index: 7 }, 
      { delay: true },
      { level: 1, index: 7 },
      { level: 1, index: 6 },
      { level: 1, index: 3 },
      { level: 1, index: 6 },
      { delay: true },

      { level: 1, index: 6 }, // 認真呼喚我姓名
      { level: 1, index: 7 },
      { delay: true },
      { level: 1, index: 6 },
      { level: 1, index: 7 },
      { level: 2, index: 3 },
      { delay: true },
      { level: 2, index: 2 },
      { level: 2, index: 1 },
      { delay: true },
      { stop: true },
      { stop: true },
      { level: 1, index: 3 },// 愛上你的時候不懂感情
      { level: 1, index: 3 },
      { level: 1, index: 5 },
      { level: 1, index: 5 },
      { level: 2, index: 1 },
      { level: 2, index: 1 },
      { level: 1, index: 7 },
      { delay: true },
      { level: 1, index: 7 },
      { level: 1, index: 6 },
      { level: 1, index: 3 },
      { level: 1, index: 6 },
      { level: 1, index: 6 },
      { delay: true },
      { stop: true },
      { level: 1, index: 6 },
      { level: 1, index: 6 },
      { level: 1, index: 7 },
      { level: 1, index: 7 },
      { level: 2, index: 3 },
      { level: 2, index: 3 },
      { level: 1, index: 7 },
      { delay: true },
      { level: 1, index: 5 },
      { level: 1, index: 3 },
      { level: 1, index: 5 },
      { delay: true },
      { stop: true },
      { level: 1, index: 3 },
      { level: 1, index: 3 },
      { level: 1, index: 5 },
      { level: 1, index: 5 },
      { level: 2, index: 1 },
      { level: 2, index: 1 },
      { level: 1, index: 7 },
      { delay: true },
      { level: 1, index: 7 },
      { level: 1, index: 6 },
      { level: 1, index: 3 },
      { level: 1, index: 6 },
      { level: 1, index: 6 },
      { delay: true },
      { stop: true },
      { level: 1, index: 6 },
      { level: 1, index: 7 },
      { delay: true },
      { level: 1, index: 6 },
      { level: 1, index: 7 },
      { level: 2, index: 3 },
      { level: 2, index: 3 },
      { level: 2, index: 2 },
      { level: 2, index: 1 },
      { delay: true },
      { delay: true },
      { stop: true },
      { level: 2, index: 3 },
      { level: 2, index: 2 },
      { level: 2, index: 1 },
      { delay: true },
      { level: 1, index: 7 },
      { level: 1, index: 6 },
      { level: 1, index: 6 },
      { level: 1, index: 6 },
      { level: 1, index: 6 },
      { level: 2, index: 3 },
      { level: 2, index: 2 },
      { delay: true },
      { level: 2, index: 2 },
      { stop: true },
      { stop: true },
      { level: 2, index: 2 },
      { level: 2, index: 1 },
      { level: 1, index: 7 },
      { delay: true },
      { level: 1, index: 6 },
      { stop: true },
      { stop: true },
      { level: 1, index: 5 },
      { level: 1, index: 5 },
      { delay: true },
      { level: 1, index: 3 },
      { level: 1, index: 5 },
      { level: 2, index: 2 },
      { level: 2, index: 1 },
      { delay: true },
      { delay: true },
      { stop: true },
      { stop: true },
      { level: 2, index: 1 },
      { level: 2, index: 1 },
      { level: 1, index: 5 },
      { level: 1, index: 5 },
      { level: 1, index: 1 },
      { level: 1, index: 3 },
      { level: 1, index: 2 },
      { level: 1, index: 6 },
      { delay: true },
      { stop: true },
      { stop: true },
      { level: 1, index: 6 },
      { delay: true },
      { level: 1, index: 6 },
      { level: 1, index: 6 },
      { level: 1, index: 6 },
      { level: 2, index: 1 },
      { level: 2, index: 1 },
      { level: 1, index: 6 },
      { level: 2, index: 1 },
      { level: 1, index: 6 },
      { delay: true },
      { stop: true },
      { level: 2, index: 1 },
      { level: 2, index: 1 },
      { level: 2, index: 1 },
      { level: 2, index: 1 },
      { level: 2, index: 3 },
      { level: 2, index: 2 },
      { level: 2, index: 2 },

      { delay: true },
      { delay: true },
      { stop: true },
      { level: 1, index: 5 },
      { level: 2, index: 3 },
      { level: 2, index: 2 },
      { level: 2, index: 1 },
      { level: 2, index: 2 },
      { delay: true },
      { level: 2, index: 3 },
      { level: 1, index: 5 },
      { level: 2, index: 2 },
      { level: 2, index: 3 },
      { delay: true },
      { level: 1, index: 5 },
      { level: 2, index: 2 },
      { level: 2, index: 3 },
      { delay: true },
      { level: 2, index: 3 },
      { level: 2, index: 2 },
      { level: 2, index: 2 },
      { level: 2, index: 3 },
      { level: 2, index: 4 },
      // { delay: true },
      { level: 2, index: 3 },
      { level: 2, index: 2 },
      { level: 1, index: 7 },
      // { stop: true },
      { level: 2, index: 1 },
      { level: 1, index: 3 },
      { level: 1, index: 6 },
      { level: 2, index: 1 },
      { delay: true },
      { level: 1, index: 3 },
      { level: 1, index: 6 },
      { level: 1, index: 7 },
      { level: 1, index: 7 },
      { level: 1, index: 7 },
      { level: 2, index: 3 },
      { level: 2, index: 5 }, 
      { level: 2, index: 3 },
      { level: 2, index: 1 },
      { level: 1, index: 7 }, 
      { level: 1, index: 6 }, 
      { level: 2, index: 4 },
      { level: 2, index: 4 },
      { delay: true },
      { level: 2, index: 5 },
      { level: 2, index: 4 },
      { level: 2, index: 3 },
      { level: 1, index: 5 },
      { level: 2, index: 3 },
      { level: 2, index: 3 },
      { delay: true },
      { level: 2, index: 4 },
      { level: 2, index: 3 },
      { level: 2, index: 1 },
      { level: 1, index: 4 },
      { delay: true },
      { level: 2, index: 2 },
      { level: 2, index: 2 },
      { delay: true },
      { level: 2, index: 2 },
      { delay: true },
      { level: 2, index: 2 },
      { level: 2, index: 1 },
      { level: 2, index: 3 },
      { delay: true },
      { level: 2, index: 2 },
      { level: 2, index: 1 },
      { level: 2, index: 3 },
      { delay: true },
      { level: 2, index: 2 },
      { level: 2, index: 1 },
      { level: 2, index: 1 },
      { delay: true },
      { level: 2, index: 3 },
      { level: 1, index: 5 },
      { level: 2, index: 2 },
      { level: 2, index: 3 },
      { delay: true },
      { level: 1, index: 5 },
      { level: 2, index: 2 },
      { level: 2, index: 3 },
      { delay: true },
      { level: 2, index: 3 },
      { level: 2, index: 2 },
      { delay: true },
      { level: 2, index: 3 },
      { level: 2, index: 4 },
      { delay: true },
      { level: 2, index: 3 },
      { level: 2, index: 2 },
      { level: 1, index: 7 },
      { delay: true },
      { level: 2, index: 1 },
      { level: 1, index: 3 },
      { level: 1, index: 6 },
      { level: 2, index: 1 },
      { delay: true },
      { level: 1, index: 3 },
      { level: 1, index: 6 },
      { level: 1, index: 7 },
      { delay: true },
      { level: 1, index: 7 },
      { level: 1, index: 7 },
      { level: 2, index: 3 },
      { level: 2, index: 5 },
      { delay: true },
      { level: 2, index: 3 },
      { level: 2, index: 1 },
      { level: 1, index: 7 },
      { delay: true },
      { level: 1, index: 6 },
      { level: 2, index: 4 },
      { level: 2, index: 4 },
      { delay: true },
      { stop: true },
      { level: 2, index: 5 },
      { level: 2, index: 4 },
      { level: 2, index: 3 },
      { level: 2, index: 5 },
      { level: 2, index: 3 },
      { delay: true },
      { level: 2, index: 3 },
      { delay: true },
      { stop: true },
      { level: 2, index: 4 },
      { level: 2, index: 3 },
      { level: 2, index: 1 },
      { level: 2, index: 4 },
      { level: 2, index: 2 },
      { level: 2, index: 2 },
      { delay: true },
      { delay: true },
      { level: 2, index: 3 },
      { level: 2, index: 1 },
      { level: 2, index: 1 },
      { level: 2, index: 3 },
      { level: 2, index: 2 },
      { delay: true },
      { level: 2, index: 1 },
      { delay: true },
    ]);
複製代碼

關注公衆號《不同的前端》,以不同的視角學習前端,快速成長,一塊兒把玩最新的技術、探索各類黑科技

相關文章
相關標籤/搜索