Magenta 音乐生成实用指南(三)
原文:
annas-archive.org/md5/5d28aa20168e891a3113f761b50d6ecc译者:飞龙
第四部分:让你的模型与其他应用程序交互
本节通过演示如何让 Magenta 与其他应用程序(如浏览器、数字音频工作站(DAW)或 MIDI 控制器)进行通信,解释了 Magenta 在音乐制作环境中的应用。通过本节内容,你将能够使用 MIDI 接口和 Magenta.js。
本节包含以下章节:
-
第八章,在浏览器中使用 Magenta.js
-
第九章,让 Magenta 与音乐应用程序交互
第十章:使用 Magenta.js 在浏览器中运行 Magenta。
本章将介绍 Magenta.js,这是 Magenta 的 JavaScript 实现,由于它运行在浏览器中并且可以作为网页共享,因此在易用性上获得了广泛的关注。我们将介绍 TensorFlow.js,它是构建 Magenta.js 的技术,并展示 Magenta.js 中可用的模型,包括如何转换我们之前训练的模型。然后,我们将使用 GANSynth 和 MusicVAE 创建小型 Web 应用程序,分别用于采样音频和序列。最后,我们将看到 Magenta.js 如何通过 Web MIDI API 和 Node.js 与其他应用程序交互。
本章将涵盖以下主题:
-
介绍 Magenta.js 和 TensorFlow.js。
-
创建 Magenta.js Web 应用程序。
-
使 Magenta.js 与其他应用程序交互。
技术要求。
本章将使用以下工具:
-
使用 命令行 或 Bash 从终端启动 Magenta。
-
Python 和 Magenta 用于将训练好的模型转换为 Magenta.js 可用格式。
-
使用 TensorFlow.js 和 Magenta.js 在浏览器中创建音乐生成应用程序。
-
使用 JavaScript、HTML 和 CSS 编写 Magenta.js Web 应用程序。
-
一个 最新版本的浏览器(Chrome、Firefox、Edge、Safari),以支持最新的 Web API。
-
Node.js 和 npm 用于安装 Magenta.js 及其依赖项(服务器端)。
-
使用 FluidSynth 从浏览器中播放生成的 MIDI。
在 Magenta.js 中,我们将使用 Music RNN 和 MusicVAE 模型来生成 MIDI 序列,使用 GANSynth 进行音频生成。我们将深入探讨它们的使用方法,但如果你觉得需要更多信息,Magenta.js 源代码中的 Magenta.js Music README(github.com/tensorflow/…)是一个不错的起点。你还可以查看 Magenta.js 代码,它有详细的文档。最后一部分 进一步阅读 中也提供了额外的内容。
本章的代码位于本书 GitHub 代码库的 Chapter08 文件夹中,位置在 github.com/PacktPublis…。所用示例和代码片段假设你在章节文件夹中。对于本章,你应该在开始之前运行 cd Chapter08。
查看以下视频,观看代码演示:
占位符链接。
介绍 Magenta.js 和 TensorFlow.js。
在前几章中,我们已经介绍了 Python 中的 Magenta、其使用方法和内部工作原理。现在我们将关注 Google 的 Magenta.js,它是 Magenta 在 JavaScript 中的精简实现。Magenta 和 Magenta.js 各有优缺点;让我们进行比较,看看在不同使用场景下应该选择哪一个。
Magenta.js 应用程序易于使用和部署,因为它在浏览器中执行。开发和部署一个 Web 应用程序非常简单:你只需要一个 HTML 文件和一个 Web 服务器,应用程序就可以让全世界看到和使用。这是基于浏览器的应用程序的一大优势,因为它不仅让我们能够轻松创建自己的音乐生成应用程序,而且使得协作使用变得更加容易。有关流行 Magenta.js Web 应用程序的精彩示例,请参见本章末尾的 进一步阅读 部分。
这就是 Web 浏览器的强大之处:每个人都有一个,而且网页不需要安装即可运行。Magenta.js Web 应用程序的缺点是它也运行在浏览器中:浏览器并不是处理高质量、实时音频的最佳场所,而且使你的应用程序与传统音乐制作工具(如 数字音频工作站 (DAWs))交互变得更加困难。
随着内容的深入,我们将逐步了解在浏览器中工作的具体细节。首先,我们将在 在浏览器中介绍 Tone.js 用于声音合成 部分中,了解 Web Audio API 的使用。接着,我们将在 使用 Web Workers API 将计算从 UI 线程卸载 部分中,讨论如何让实时音频变得更加轻松。最后,我们将在 使 Magenta.js 与其他应用程序交互 部分中,讨论如何让 Magenta.js 与其他音乐应用程序互动。
在浏览器中介绍 TensorFlow.js 机器学习
首先,让我们介绍 TensorFlow.js(www.tensorflow.org/js),Magenta.js 构建的项目。正如其名称所示,TensorFlow.js 是 TensorFlow 的 JavaScript 实现,使得在浏览器中使用和训练 模型成为可能。也可以导入并运行来自 TensorFlow SavedModel 或 Keras 的预训练模型。
使用 TensorFlow.js 很简单。你可以使用 script 标签,如以下代码块所示:
<html>
<head>
<script src="img/tf.min.js"></script>
<script>
const model = tf.sequential();
model.add(tf.layers.dense({units: 1, inputShape: [1]}));
model.compile({loss: 'meanSquaredError', optimizer: 'sgd'});
</script>
</head>
<body>
</body>
</html>
另外,你也可以使用 npm 或 yarn 命令来运行以下代码块:
import * as tf from '@tensorflow/tfjs';
const model = tf.sequential();
model.add(tf.layers.dense({units: 1, inputShape: [1]}));
model.compile({loss: 'meanSquaredError', optimizer: 'sgd'});
注意在这两个代码片段中 tf 变量的使用,它是通过脚本导入的(在本章的示例中我们将看到更多 tf 的使用)。我们不会特别研究 TensorFlow.js,但我们将在 Magenta.js 代码中使用它。
TensorFlow.js 另一个优点是它使用 WebGL(www.khronos.org/registry/we…)进行计算,这意味着它是 图形处理单元(GPU)加速的(如果你有 GPU),且无需安装 CUDA 库。数学运算在 WebGL 着色器中实现,张量被编码为 WebGL 纹理,这是 WebGL 的一种非常巧妙的使用方法。我们无需做任何事来启用 GPU 加速,因为 TensorFlow.js 后端会为我们处理。当使用 Node.js 服务器时,TensorFlow C API 用于硬件加速,这意味着也可以使用 CUDA 库。
使用 WebGL 有一些注意事项,最显著的是计算在某些情况下可能会阻塞 UI 线程,以及张量分配所使用的内存必须在使用后进行回收(释放)。关于计算线程,我们将在讨论 Web Workers 时更深入地探讨。关于内存管理,我们将在代码中展示正确的使用方法。有关这些问题的更多信息,请参见 进一步阅读 部分。
在浏览器中生成音乐的 Magenta.js 介绍
现在我们理解了 TensorFlow.js,接下来讨论 Magenta.js。首先,我们需要了解 Magenta.js 能做什么,不能做什么。目前,Magenta.js 中无法训练模型(除了在 MidiMe 中的部分训练),但我们在前一章中训练的模型可以轻松转换和导入。Magenta.js 的另一个限制是并非所有模型都包含在内,但最重要的模型都包括在内。在编写 Magenta.js 代码时,我们会发现大部分已覆盖的概念都在其中,只是语法有所不同。
以下是 Magenta.js 中一些预训练模型的概述:
-
Onsets and Frames 用于钢琴转录,将原始音频转换为 MIDI
-
Music RNN(长短期记忆(LSTM)网络)用于单音和多音 MIDI 生成,包括 Melody RNN、Drums RNN、Improv RNN 和 Performance RNN 模型
-
MusicVAE 用于单音或三重音采样与插值,另包括 GrooVAE
-
Piano Genie 将 8 键输入映射到完整的 88 键钢琴
我们已经在前面的章节中讨论了这些模型。我们可以在 Magenta.js 源代码中找到预训练检查点列表,路径为 music/checkpoints/checkpoints.json 文件,或者在托管版本中,通过 goo.gl/magenta/js-… 访问。我们使用的大多数检查点(或模型包)都包含在 Magenta.js 中,还有一些新增加的模型,例如更长的 4 小节 MusicVAE 和 GrooVAE 模型。
将训练好的模型转换为 Magenta.js 格式
使用预训练模型非常好,但我们也可以导入我们自己训练的模型,比如我们在上一章中训练的模型——第七章,训练 Magenta 模型。我们通过使用 checkpoint_converted.py 脚本实现这一点,该脚本将 Magenta 检查点中的权重转储到 Magenta.js 可以使用的格式。
你可以在本章节的源代码中找到这段代码,文件名为 chapter_08_example_01.html。源代码中有更多的注释和内容,你应该去查看它。
让我们通过以下步骤,将一个简单的 RNN 模型转换为适用于 Magenta.js 的格式:
- 首先,我们需要从 Magenta.js 获取
checkpoint_converter.py脚本。最简单的方法是直接从 GitHub 上的源代码下载该脚本,如下所示:
curl -o "checkpoint_converter.py" "https://raw.githubusercontent.com/tensorflow/magenta-js/master/scripts/checkpoint_converter.py"
这应该会在本地创建 checkpoint_converter.py 文件。
- 现在,我们需要 TensorFlow.js Python 打包文件,这是
checkpoint_converter.py脚本所依赖的。运行以下代码:
# While in your Magenta conda environment
pip install tensorflowjs
- 我们现在可以运行转换脚本,例如,使用我们之前训练的 DrumsRNN 模型(将
PATH_TO_TRAINING_DIR替换为合适的值),如下所示:
python checkpoint_converter.py "PATH_TO_TRAINING_DIR/drums_rnn_dance_drums/logdir/run1_small/train/model.ckpt-20000" "checkpoints/drums_rnn_dance_small"
这将创建 checkpoints/drums_rnn_dance_small 目录,其中包含一个 JSON 元数据文件和将由 TensorFlow.js 加载的检查点二进制文件。
请记住,在引用 TensorFlow 检查点时,您需要提供前缀——例如,model.ckpt-20000,但不应加上 .data、.index 或 .meta。
- 然后,我们需要创建一个 JSON 配置文件,描述模型的配置。打开
checkpoints/drums_rnn_dance_small/config.json文件,并输入以下内容:
{
"type": "MusicRNN",
"dataConverter": {
"type": "DrumsConverter",
"args": {}
}
}
这是 DrumsRNN 模型的一个最小示例,没有进一步的配置。请注意,即使没有提供任何参数,dataConverter 的 args 键也是必要的。dataConverter 的 type 是 DataConverter 的子类之一,位于 Magenta.js 源代码中的 music/src/core 文件夹下的 data.ts 文件中。其他可能的数据转换器包括 MelodyConverter、TrioConverter 或 GrooveConverter。
其他模型和转换器将需要更多的配置。找到适当配置的最简单方法是找到一个类似的 Magenta 预训练模型,并使用类似的值。为此,按照 下载预训练模型到本地 部分操作,并在下载的 config.json 文件中找到所需的信息。
- 我们的自定义模型现在已转换为 TensorFlow.js 可以理解的格式。接下来,让我们创建一个小型网页,导入并初始化该模型进行测试,如下所示:
<html lang="en">
<body>
<script src="img/magentamusic.js"></script>
<script>
// Initialize a locally trained DrumsRNN model from the local directory
// at: checkpoints/drums_rnn_dance_small
async function startLocalModel() {
const musicRnn = new mm.MusicRNN("http://0.0.0.0:8000/" +
"checkpoints/drums_rnn_dance_small");
await musicRnn.initialize();
}
// Calls the initialization of the local model
try {
Promise.all([startLocalModel()]);
} catch (error) {
console.error(error);
}
</script>
</body>
</html>
不必过于担心 HTML 页面中的内容,因为它将在接下来的章节中得到详细解释。这里重要的是,MusicRNN 构造函数(mm.MusicRNN("URL"))正在加载我们转换后的 DrumsRNN 检查点到 MusicRNN 模型中。
你可能注意到检查点的 URL 是本地的,位于http://0.0.0.0:8000。这是因为大多数浏览器实现了跨源资源共享(CORS)限制,其中之一是本地文件只能获取以统一资源标识符(URI)方案http或https开头的资源。
- 绕过这一点的最简单方法是本地启动一个 web 服务器,如下所示:
python -m http.server
这将启动一个 web 服务器,在http://0.0.0.0:8000提供当前文件夹的内容,这意味着前面代码片段中的 HTML 文件将通过http://0.0.0.0:8000/example.html提供,且我们的检查点将位于http://0.0.0.0:8000/checkpoints/drums_rnn_dance_small。
- 打开 HTML 文件并检查控制台。你应该会看到以下内容:
* Tone.js v13.8.25 *
MusicRNN Initialized model in 0.695s
这意味着我们的模型已成功初始化。
本地下载预训练模型
本地下载预训练模型很有用,如果我们想自己提供模型或检查config.json的内容:
- 首先,我们需要 Magenta.js 中的
checkpoint_converter.py脚本。最简单的方法是直接从 GitHub 的源代码下载该脚本,如下所示:
curl -o "checkpoint_downloader.py" "https://raw.githubusercontent.com/tensorflow/magenta-js/master/scripts/checkpoint_downloader.py"
这应该会在本地创建checkpoint_converter.py文件。
- 然后,我们可以通过输入以下代码来调用该脚本:
python checkpoint_downloader.py "https://storage.googleapis.com/magentadata/js/checkpoints/music_vae/mel_16bar_small_q2" "checkpoints/music_vae_mel_16bar_small_q2"
这将下载mel_16bar_small_q2 MusicVAE 预训练模型到checkpoints文件夹中。
在浏览器中引入 Tone.js 进行声音合成
在本章中,你将听到在浏览器中生成的音频,这意味着音频合成,类似于我们在前几章中使用 FluidSynth 来播放 MIDI 文件的方式,是在浏览器中发生的,使用的是 Web Audio API。
Web Audio API(www.w3.org/TR/webaudio…)提供了相当低级的概念,通过音频节点来处理声音源、转换和路由。首先,我们有一个声音源,它提供一组声音强度(有关这一点的复习,请参见第一章,关于 Magenta 和生成艺术的介绍),它可以是一个声音文件(样本)或一个振荡器。然后,声音源节点可以连接到一个转换节点,如增益(用于改变音量)。最后,结果需要连接到一个目标(输出),使声音能够通过扬声器播放出来。
该规范已经相当成熟,列为W3C 候选推荐,2018 年 9 月 18 日,因此一些实现细节可能会发生变化,但可以认为它是稳定的。就支持而言,所有主要浏览器都支持 Web Audio API,这非常好。有关更多信息,请参阅进一步阅读部分。
我们不会直接使用 Web Audio API,而是使用 Tone.js (tonejs.github.io),这是一个建立在 Web Audio API 之上的 JavaScript 库,提供更高层次的功能。使用 Tone.js 的另一个优势是,它能够适应底层 Web Audio API 的变化。
由于 Web Audio API 在不同浏览器中的实现有所不同,音频质量可能会有所不同。例如,在 Firefox 中叠加多个来自 GANSynth 的音频样本时会出现音频削波问题,但在 Chrome 中则可以正常工作。请记住,对于专业级别的音频质量,浏览器中的音频合成可能不是最佳选择。
创建一个 Magenta.js Web 应用
现在我们已经介绍了 Magenta.js 的相关概念,接下来我们将使用 Magenta.js 创建一个 Web 应用。让我们创建一个 Web 应用,使用 MusicVAE 生成三种乐器(鼓组、低音和主音),并且可以将主乐器替换为 GANSynth 生成的乐器。
我们将一步步构建这个应用。首先,我们将做一个生成乐器的应用,使用 GANSynth。然后,我们将创建一个可以采样三重奏序列的应用。最后,我们将把这两个应用合并在一起。
在浏览器中使用 GANSynth 生成乐器
在我们示例的第一部分,我们将使用 GANSynth 来采样单个乐器音符,这些音符是时长为 4 秒的短音频片段。我们将能够将多个音频片段叠加,从而产生有趣的效果。
首先,我们将创建 HTML 页面并导入所需的脚本。接下来,我们将编写 GANSynth 采样代码,并详细解释每一步。最后,我们将通过聆听生成的音频来完成示例。
编写页面结构
我们将保持页面结构和样式的简洁,专注于 Magenta.js 的代码。
你可以在本章源代码中的 chapter_08_example_02.html 文件中找到这段代码。源代码中有更多的注释和内容,你应该去查看一下。
首先,让我们创建页面结构并导入所需的脚本,如下所示:
<html lang="en">
<body>
<div>
<button disabled id="button-sample-gansynth-note">
Sample GANSynth note
</button>
<div id="container-plots"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/@magenta/music@1.12.0/dist/magentamusic.min.js"></script>
<script>
// GANSynth code
</script>
</body>
</html>
页面结构只包含一个按钮,用于调用 GANSynth 生成音频,以及一个容器,用于绘制生成的频谱图。
在浏览器中使用 Magenta.js 有两种方式,具体如下:
-
我们可以在
dist/magentamusic.min.js中导入整个 Magenta.js 音乐库。在 Magenta 文档中,这被称为 ES5 bundle 方法。这将包括 Magenta.js(绑定为mm)及其所有依赖项,包括 TensorFlow.js(绑定为mm.tf)和 Tone.js(绑定为mm.Player.tone)。 -
我们可以仅导入我们需要的 Magenta.js 元素,这些元素位于
es6文件夹中。在 Magenta 文档中,这称为 ES6 打包 方法。例如,如果我们只需要 GANSynth 模型,我们需要导入 Tone.js(绑定到Tone),TensorFlow.js(绑定到tf),Magenta.js 核心(绑定到core),以及 Magenta.js GANSynth(绑定到gansynth)。
在这里我们不讨论 ES5 和 ES6 打包文件之间的差异。只需记住,最简单的方法是使用 ES5 打包方式,导入一个包含所有内容的大文件。如果你想对发送到客户端的内容有更多控制(例如,出于性能原因),你将需要使用 ES6 打包方式。请记住,两种方法之间的模块绑定不同,因此如果你更改了导入,你需要调整代码。
以下是仅包含 GANSynth 模型的 ES6 打包导入:
<script src="img/Tone.min.js"></script>
<script src="img/tf.min.js"></script>
<script src="img/core.js"></script>
<script src="img/gansynth.js"></script>
这仅导入 GANSynth 模型,模型可以通过 new gansynth.GANSynth(...) 实例化。在使用 ES6 模块时,我们需要单独导入每个脚本。对于我们的示例,这些脚本是 Tone.js、TensorFlow.js、Magenta.js 核心和 GANSynth。
对于我们的示例,我们将坚持使用 ES5 打包方式,但如果你愿意,可以使用 ES6 打包方式。在我们的示例中,我们将展示每种方法之间代码的不同之处。
你可以在 chapter_08_example_02_es6.html 文件中找到本例的 ES6 代码,该文件位于本章的源代码中。
现在,让我们编写 GANSynth 代码(在 GANSynth code 注释中),并解释每一步。
使用 GANSynth 采样音频
现在我们已经正确导入了 Magenta.js,我们可以按照以下步骤编写 GANSynth 音频生成代码:
- 首先,我们将初始化 DOM 元素并初始化 GANSynth,如下所示:
// Get DOM elements
const buttonSampleGanSynthNote = document
.getElementById("button-sample-gansynth-note");
const containerPlots = document
.getElementById("container-plots");
// Starts the GANSynth model and initializes it. When finished, enables
// the button to start the sampling
async function startGanSynth() {
const ganSynth = new mm.GANSynth("https://storage.googleapis.com/" +
"magentadata/js/checkpoints/gansynth/acoustic_only");
await ganSynth.initialize();
window.ganSynth = gansynth;
buttonSampleGanSynthNote.disabled = false;
}
在这里,我们使用 mm.GANSynth(...) 实例化 GANSynth。记住,当使用 ES5 模块导入时,Magenta.js 上下文位于 mm 变量下。检查点的 URL 与我们在上一章中使用的相同——第五章,使用 NSynth 和 GANSynth 生成音频。如果你想要更多信息,请参考那一章。我们还将 ganSynth 引用设置为全局变量,以便稍后可以轻松调用。
使用 Magenta.js ES6 打包时,我们将拥有以下代码:
const ganSynth = new gansynth.GANSynth("https://storage.googleapis.com/" +
"magentadata/js/checkpoints/gansynth/acoustic_only");
对于 ES6 打包,模块变量是 gansynth.GANSynth,而不是 mm.GANSynth。
- 现在,让我们编写一个异步函数,使用
canvas将生成的频谱图插入网页中,如下所示:
// Plots the spectrogram of the given channel
// see music/demos/gansynth.ts:28 in magenta.js source code
async function plotSpectra(spectra, channel) {
const spectraPlot = mm.tf.tidy(() => {
// Slice a single example.
let spectraPlot = mm.tf.slice(spectra, [0, 0, 0, channel], [1, -1, -1, 1])
.reshape([128, 1024]);
// Scale to [0, 1].
spectraPlot = mm.tf.sub(spectraPlot, mm.tf.min(spectraPlot));
spectraPlot = mm.tf.div(spectraPlot, mm.tf.max(spectraPlot));
return spectraPlot;
});
// Plot on canvas.
const canvas = document.createElement("canvas");
containerPlots.appendChild(canvas);
await mm.tf.browser.toPixels(spectraPlot, canvas);
spectraPlot.dispose();
}
此方法创建一个频谱图,并将其插入我们之前声明的 containerPlots 元素中的 canvas 元素。它将继续为每次生成添加频谱图。
你可能已经注意到在示例中使用了 tf.tidy 和 dispose。使用这些方法是为了避免 TensorFlow.js 代码中的内存泄漏。这是因为 TensorFlow.js 使用 WebGL 来进行计算,而WebGL 资源在使用后需要显式回收。任何 tf.Tensor 在使用后都需要通过使用 dispose 来进行清理。tf.tidy 方法可以在执行完函数后,自动清理所有未返回的张量。
你可能会想知道在之前的 JavaScript 代码中,async 和 await 关键字是什么意思。这两个关键字标志着异步方法的使用。当调用一个被标记为 async 的方法时,表示它是异步的,调用者需要使用 await 来标记调用,这意味着它会等待(阻塞)直到返回一个值。await 关键字只能在 async 方法中使用。在我们的示例中,mm.tf.browser.toPixels 方法被标记为 async,因此我们需要使用 await 等待它的返回。调用一个 async 方法而不使用 await 可以通过 Promise 语法完成—Promise.all([myAsyncMethod()])。
Promise 是在 JavaScript 中引入的,目的是解决编写异步代码时遇到的一个反复出现的问题:回调地狱。回调地狱是一个问题,当多个关联的调用都是异步时,会导致嵌套的回调(地狱般的回调)。
Promise 非常棒,因为它们提供了一个干净的机制来处理复杂的异步调用链,并且有适当的错误处理。然而,它们有点冗长,这就是为什么引入了 async 和 await 关键字作为语法糖,以缓解使用 Promise 时的常见用例。
- 然后,我们编写一个异步函数,从 GANSynth 中采样一个音符,播放它,并使用我们之前的方法绘制它,如下所示:
// Samples a single note of 4 seconds from GANSynth and plays it repeatedly
async function sampleGanNote() {
const lengthInSeconds = 4.0;
const sampleRate = 16000;
const length = lengthInSeconds * sampleRate;
// The sampling returns a spectrogram, convert that to audio in
// a tone.js buffer
const specgrams = await ganSynth.randomSample(60);
const audio = await ganSynth.specgramsToAudio(specgrams);
const audioBuffer = mm.Player.tone.context.createBuffer(
1, length, sampleRate);
audioBuffer.copyToChannel(audio, 0, 0);
// Play the sample audio using tone.js and loop it
const playerOptions = {"url": audioBuffer, "loop": true, "volume": -25};
const player = new mm.Player.tone.Player(playerOptions).toMaster();
player.start();
// Plots the resulting spectrograms
await plotSpectra(specgrams, 0);
await plotSpectra(specgrams, 1);
}
我们首先使用 randomSample 方法从 GANSynth 中采样,传入基准音高 60(即 C4)作为参数。这告诉模型从与该音高相对应的值进行采样。然后,返回的频谱图使用 specgramsToAudio 转换为音频。最后,我们使用 Tone.js 的音频缓冲区来播放该采样,通过实例化一个新播放器并使用音频缓冲区。由于我们为每个采样实例化一个新播放器,因此每个新的音频采样都会叠加在其他采样之上。
实例化播放器的代码 mm.Player.tone.Player 有些复杂,因为我们首先需要找到已经被 Magenta.js 对象通过 mm.Player.tone 实例化的 Tone.js 引用(这里,Player 引用是 Magenta.js 的一个类)。
使用 ES6 打包文件更为直接,如这里所示:
const player = new Tone.Player(playerOptions).toMaster();
由于 Magenta.js 的 ES6 打包文件未包含 Tone.js,它需要自行初始化,并可以直接通过 Tone 变量进行引用。
- 最后,我们通过将按钮绑定到一个操作并初始化 GANSynth 来总结我们的示例,如下所示:
// Add on click handler to call the GANSynth sampling
buttonSampleGanSynthNote.addEventListener("click", () => {
sampleGanNote();
});
// Calls the initialization of GANSynth
try {
Promise.all([startGanSynth()]);
} catch (error) {
console.error(error);
}
首先,我们将按钮绑定到 sampleGanNote 方法,然后我们初始化 GANSynth,使用 startGanSynth 方法。
启动网页应用
现在我们的网页应用已经准备好,可以测试代码了。让我们用浏览器打开我们创建的 HTML 页面。我们应该能看到一个与下图相似的页面:
在前面的图中,我们已经生成了一些 GANSynth 样本。每次生成都会绘制两个频谱图,并将之前的保持在页面上。在前面的截图右侧,您可以在控制台调试器中看到 Tone.js 和 GANSynth 初始化。当完成后,Sample GANSynth note 按钮将启用。
继续生成声音:当你叠加很多声音时,会得到非常有趣的效果。恭喜你——你已经完成了第一个 Magenta.js 网页应用!
使用 MusicVAE 生成三重奏
我们现在将使用 Magenta.js 中的 MusicVAE 模型来生成一些序列,并直接在浏览器中播放,使用 Tone.js。我们将使用的检查点是一个 trio 模型,这意味着我们将同时生成三种序列:打击乐、低音和主旋律。
您可以在本章源代码的 chapter_08_example_03.html 文件中找到这段代码。源代码中有更多的注释和内容,您应该去查看一下。
由于这段代码与上一节类似,我们不会逐一讲解所有内容,但会解释主要的区别:
- 首先,我们定义页面结构和脚本导入,如下所示:
<html lang="en">
<body>
<div>
<button disabled id="button-sample-musicae-trio">
Sample MusicVAE trio
</button>
<canvas id="canvas-musicvae-plot"></canvas>
</div>
<script src="https://cdn.jsdelivr.net/npm/@magenta/music@1.12.0/dist/magentamusic.min.js"></script>
<script>
// MusicVAE code
</script>
</body>
</html>
页面结构与上一节相同。我们将在 MusicVAE 代码 注释中填入代码。
- 接下来,让我们初始化 MusicVAE 模型,如下所示:
// Get DOM elements
const buttonSampleMusicVaeTrio = document
.getElementById("button-sample-musicae-trio");
const canvasMusicVaePlot = document
.getElementById("canvas-musicvae-plot");
// Starts the MusicVAE model and initializes it. When finished, enables
// the button to start the sampling
async function startMusicVae() {
const musicvae = new mm.MusicVAE("https://storage.googleapis.com/" +
"magentadata/js/checkpoints/music_vae/trio_4bar");
await musicvae.initialize();
window.musicvae = musicvae;
buttonSampleMusicVaeTrio.disabled = false;
}
检查点的 URL 与上一章使用的相同——第四章,使用 MusicVAE 进行潜在空间插值。如果您想了解该检查点的更多信息,请参考本章。
- 我们现在创建一个新的 Tone.js 播放器来播放生成的三个序列,如下所示:
// Declares a new player that have 3 synths for the drum kit (only the
// bass drum), the bass and the lead.
class Player extends mm.BasePlayer {
bassDrumSynth = new mm.Player.tone.MembraneSynth().toMaster();
bassSynth = new mm.Player.tone.Synth({
volume: 5,
oscillator: {type: "triangle"}
}).toMaster();
leadSynth = new mm.Player.tone.PolySynth(5).toMaster();
// Plays the note at the proper time using tone.js
playNote(time, note) {
let frequency, duration, synth;
if (note.isDrum) {
if (note.pitch === 35 || note.pitch === 36) {
// If this is a bass drum, we use the kick pitch for
// an eight note and the bass drum synth
frequency = "C2";
duration = "8n";
synth = this.bassDrumSynth;
}
} else {
// If this is a bass note or lead note, we convert the
// frequency and the duration for tone.js and fetch
// the proper synth
frequency = new mm.Player.tone.Frequency(note.pitch, "midi");
duration = note.endTime - note.startTime;
if (note.program >= 32 && note.program <= 39) {
synth = this.bassSynth;
} else {
synth = this.leadSynth;
}
}
if (synth) {
synth.triggerAttackRelease(frequency, duration, time, 1);
}
}
}
这段代码扩展了 Magenta.js 中的 mm.BasePlayer 类,这很有用,因为我们只需要实现 playNote 方法来播放序列。首先,我们定义了三个合成器:bassDrumSynth、bassSynth 和 leadSynth,如下所示:
-
低音鼓合成器只播放低音鼓,由
note.isDrum属性和 MIDI 音符 35 或 36 表示,并且总是播放C2的频率,长度为 8 分音符(8n),使用 Tone.js 的MembraneSynth。请记住:在 MIDI 规范中,打击乐通道中的乐器(如低音鼓、军鼓等)是通过音符的音高来定义的——例如,音高 35 是原声低音鼓。 -
低音合成器只播放从 32 到 39 的程序,使用 Tone.js 中的
Synth和三角波形。记住:根据 MIDI 规范,程序指定了应该播放的乐器。例如,程序 1 是钢琴,而程序 33 是木吉他。 -
主音合成器使用 Tone.js 中的
PolySynth和五个音轨来演奏其他程序。
需要注意的是,对于低音和主音合成器,我们首先需要将 MIDI 音符转换为 Tone.js 的频率,使用Frequency类。
另一个需要讨论的重要内容是音符包络,它在 Tone.js 中的合成器上通过triggerAttackRelease方法使用。包络充当过滤器,允许音符在一定时间内被听到。你可以把合成器想象成始终在播放,而包络—当关闭时—不会让声音通过。当包络打开时,它允许声音被听到,使用一定的斜率,意味着声音可以慢慢(或快速)出现,并慢慢(或快速)结束。这分别被称为包络的起音和释放。每次我们调用触发方法时,合成器会根据给定的持续时间和一定的斜率发出声音。
你可能已经听说过起音衰减延音释放(ADSR)这个术语,尤其是在谈论包络时。在 Tone.js 中,我们使用的是这个概念的简化版,仅使用包络的起音和释放。如果使用完整的 ADSR 包络,我们可以更好地控制结果的形状。为了简单起见,我们的例子中将使用简化版。
- 现在让我们来采样 MusicVAE 模型,如下所示:
// Samples a trio of drum kit, bass and lead from MusicVAE and
// plays it repeatedly at 120 QPM
async function sampleMusicVaeTrio() {
const samples = await musicvae.sample(1);
const sample = samples[0];
new mm.PianoRollCanvasVisualizer(sample, canvasMusicVaePlot,
{"pixelsPerTimeStep": 50});
const player = new Player();
mm.Player.tone.Transport.loop = true;
mm.Player.tone.Transport.loopStart = 0;
mm.Player.tone.Transport.loopEnd = 8;
player.start(sample, 120);
}
首先,我们使用sample方法和参数 1 来采样 MusicVAE 模型,1 表示所需的样本数量。然后,我们使用之前声明的canvas中的mm.PianoRollCanvasVisualizer绘制结果音符序列。最后,我们以 120 QPM 启动播放器,并循环 8 秒的音序,使用 Tone.js 中的Transport类。记住,MusicVAE 模型具有固定的长度,这意味着使用 4 小节三重奏模型,我们生成 8 秒的样本,速度为 120 QPM。
- 最后,让我们通过绑定按钮到一个动作并初始化 MusicVAE 模型来完成我们的示例,如下所示:
// Add on click handler to call the MusicVAE sampling
buttonSampleMusicVaeTrio.addEventListener("click", (event) => {
sampleMusicVaeTrio();
event.target.disabled = true;
});
// Calls the initialization of MusicVAE
try {
Promise.all([startMusicVae()]);
} catch (error) {
console.error(error);
}
首先,我们将按钮绑定到sampleMusicVaeTrio方法,然后我们使用startMusicVae方法初始化 MusicVAE 模型。你可以看到我们这里使用了之前介绍的Promise.all调用来启动我们的异步代码。
- 现在我们已经准备好我们的网页应用程序,可以测试我们的代码了。让我们使用浏览器打开我们创建的 HTML 页面。我们应该能看到一个类似于以下截图的页面:
通过点击Sample MusicVAE trio按钮,MusicVAE 应该会采样一个序列,绘制出来并使用我们定义的合成器播放。生成的图形相当基础,因为它没有区分三个乐器,也没有时间或音高标记,但可以通过PianoRollCanvasVisualizer类进行自定义。
要生成一个新序列,重新加载页面以重新开始。
使用 SoundFont 来获得更真实的乐器音色
当听到生成的声音时,你可能会注意到音乐听起来有点基础或简单。这是因为我们使用了 Tone.js 的默认合成器,它的优点是易于使用,但缺点是音质不如更复杂的合成器好。记住,Tone.js 的合成器可以进行自定义,以便听起来更好。
我们可以使用 SoundFont 代替合成器。SoundFont 是多种乐器的录制音符,我们从本书一开始就一直在 FluidSynth 中使用它们。在 Magenta.js 中,我们可以使用SoundFontPlayer来代替Player实例,代码如下所示:
const player = new mm.SoundFontPlayer("https://storage.googleapis.com/" +
"magentadata/js/soundfonts/salamander"));
player.start(sequence, 120)
Magenta 团队托管的 SoundFont 列表可以在 Magenta.js 音乐文档中找到(github.com/tensorflow/…)。
演奏生成的三重奏乐器
现在,我们有了 MusicVAE 生成三种乐器的序列,以及 GANSynth 生成音频,接下来让我们让这两者协同工作。
你可以在本章的源代码中的chapter_08_example_04.html文件中找到这段代码。源代码中有更多的注释和内容,你应该去查看一下。
由于代码与上一节类似,我们不会逐一讲解所有内容,但会解释主要的区别:
- 首先,让我们定义页面结构和脚本导入,如下所示:
<html lang="en">
<body>
<div>
<button disabled id="button-sample-musicae-trio">
Sample MusicVAE trio
</button>
<button disabled id="button-sample-gansynth-note">
Sample GANSynth note for the lead synth
</button>
<canvas id="canvas-musicvae-plot"></canvas>
<div id="container-plots"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/@magenta/music@1.12.0/dist/magentamusic.min.js"></script>
<script>
// MusicVAE + GANSynth code
</script>
</body>
</html>
该页面与上一节的结构相同。我们将填充MusicVAE + GANSynth code注释中的代码。
- 然后,让我们初始化 MusicVAE 模型和 GANSynth 模型,如下所示:
// Get DOM elements
const buttonSampleGanSynthNote = document
.getElementById("button-sample-gansynth-note");
const buttonSampleMusicVaeTrio = document
.getElementById("button-sample-musicae-trio");
const containerPlots = document
.getElementById("container-plots");
const canvasMusicVaePlot = document
.getElementById("canvas-musicvae-plot");
// Starts the MusicVAE model and initializes it. When finished, enables
// the button to start the sampling
async function startMusicVae() {
const musicvae = new mm.MusicVAE("https://storage.googleapis.com/" +
"magentadata/js/checkpoints/music_vae/trio_4bar");
await musicvae.initialize();
window.musicvae = musicvae;
buttonSampleMusicVaeTrio.disabled = false;
}
// Starts the GANSynth model and initializes it
async function startGanSynth() {
const ganSynth = new mm.GANSynth("https://storage.googleapis.com/" +
"magentadata/js/checkpoints/gansynth/acoustic_only");
await ganSynth.initialize();
window.ganSynth = ganSynth
}
在这里,我们仅启用MusicVAE sampling按钮。GANSynth sampling按钮将在 MusicVAE 完成生成后启用。
-
我们保持使用相同的
plotSpectra方法(来自之前的示例)。 -
我们保持使用相同的
Player类(来自之前的示例)进行声音合成。我们可以设置leadSynth = null,因为它将被 GANSynth 生成替代,但这不是必需的。 -
我们保持使用相同的
sampleMusicVaeTrio方法(来自之前的示例),但我们还将实例化的播放器设置为全局变量,使用window.player = player,因为 GANSynth 稍后需要更改主合成器。 -
我们重写了
sampleGanNote方法(来自之前的示例),以添加一个样本播放器,如下所示:
// Samples a single note of 4 seconds from GANSynth and plays it repeatedly
async function sampleGanNote() {
const lengthInSeconds = 4.0;
const sampleRate = 16000;
const length = lengthInSeconds * sampleRate;
// The sampling returns a spectrogram, convert that to audio in
// a tone.js buffer
const specgrams = await ganSynth.randomSample(60);
const audio = await ganSynth.specgramsToAudio(specgrams);
const audioBuffer = mm.Player.tone.context.createBuffer(
1, length, sampleRate);
audioBuffer.copyToChannel(audio, 0, 0);
// Plays the sample using tone.js by using C4 as a base note,
// since this is what we asked the model for (MIDI pitch 60).
// If the sequence contains other notes, the pitch will be
// changed automatically
const volume = new mm.Player.tone.Volume(-10);
const instrument = new mm.Player.tone.Sampler({"C4": audioBuffer});
instrument.chain(volume, mm.Player.tone.Master);
window.player.leadSynth = instrument;
// Plots the resulting spectrograms
await plotSpectra(specgrams, 0);
await plotSpectra(specgrams, 1);
}
首先,我们使用 randomSample 从 GANSynth 中随机采样一个乐器,像前面的示例那样。然后,我们需要在 Tone.js 合成器中播放该样本,因此我们使用 Sampler 类,它接收一个包含每个键的样本字典。因为我们使用 MIDI 音高 60 对模型进行了采样,所以我们使用 C4 来表示生成的音频缓冲区。最后,我们通过 window.player.leadSynth = instrument 将该合成器添加到我们的播放器中。
- 让我们通过将按钮绑定到相应的操作,并初始化 MusicVAE 和 GANSynth 模型来总结我们的示例,如下所示:
// Add on click handler to call the MusicVAE sampling
buttonSampleMusicVaeTrio.addEventListener("click", (event) => {
sampleMusicVaeTrio();
event.target.disabled = true;
buttonSampleGanSynthNote.disabled = false;
});
// Add on click handler to call the GANSynth sampling
buttonSampleGanSynthNote.addEventListener("click", () => {
sampleGanNote();
});
// Calls the initialization of MusicVAE and GanSynth
try {
Promise.all([startMusicVae(), startGanSynth()]);
} catch (error) {
console.error(error);
}
这段代码将启动模型,绑定按钮,并更新按钮状态。
- 现在我们已经准备好我们的 Web 应用程序,可以测试我们的代码了。让我们使用浏览器打开我们创建的 HTML 页面。我们应该会看到一个类似于以下屏幕截图的页面:
通过按下 为主合成器采样 MusicVAE 三重奏 按钮,MusicVAE 应该会采样一个序列,绘制它,并使用我们定义的合成器进行播放。然后,可以使用 为主合成器采样 GANSynth 音符 按钮来生成一个新的声音用于主合成器,这可以多次使用。
要生成一个新的序列,重新加载页面以重新开始。
使用 Web Workers API 将计算卸载出 UI 线程
正如你从前面的示例中可能注意到的,当你使用 为主合成器采样 GANSynth 音符 按钮时,音频会冻结(你将听不到来自 MusicVAE 的任何声音),这是因为 GANSynth 正在生成它的第一个样本。
这是因为 JavaScript 的并发是基于事件循环模式构建的,这意味着 JavaScript 不是多线程的,一切都在一个称为UI 线程的单线程中执行。这是可行的,因为 JavaScript 使用非阻塞 I/O,这意味着大多数昂贵的操作可以立即完成,并通过事件和回调返回它们的值。然而,如果一个长时间的计算是同步的,它将在执行时阻塞 UI 线程,这正是 GANSynth 在生成其样本时发生的情况(有关 TensorFlow 如何使用 WebGL 处理计算的更多信息,请参见前面的 在浏览器中使用 TensorFlow.js 进行机器学习 部分)。
解决此问题的一种方法是Web Workers API(html.spec.whatwg.org/multipage/w…),由Web 超文本应用技术工作组(WHATWG)规范,该 API 使得将计算卸载到不会影响 UI 线程的另一个线程成为可能。Web Worker 基本上是一个 JavaScript 文件,它从主线程启动并在自己的线程中执行。它可以与主线程发送和接收消息。Web Workers API 已经成熟,并且在浏览器中得到了很好的支持。你可以在 进一步阅读 部分了解更多关于 Web Worker 的信息。
你可以在本章的源代码中找到 chapter_08_example_05.html 和 chapter_09_example_05.js 文件中的代码。源代码中有更多的注释和内容——你应该去查看一下。
不幸的是,在撰写本文时,Magenta 的某些部分与 Web 工作线程的兼容性不佳。我们将展示一个使用 MusicVAE 模型的示例,但我们无法展示同样的示例来使用 GANSynth,因为该模型无法在 Web 工作线程中加载。我们仍然提供此示例,因为它可以作为以后使用的基础:
- 让我们编写主页面的代码。我们将只包括完整 HTML 页面中的 JavaScript 代码,因为前面章节已经涵盖了其他部分。请按以下步骤进行:
// Starts a new worker that will load the MusicVAE model
const worker = new Worker("chapter_09_example_05.js");
worker.onmessage = function (event) {
const message = event.data[0];
if (message === "initialized") {
// When the worker sends the "initialized" message,
// we enable the button to sample the model
buttonSampleMusicVaeTrio.disabled = false;
}
if (message === "sample") {
// When the worked sends the "sample" message,
// we take the data (the note sequence sample)
// from the event, create and start a new player
// using the sequence
const data = event.data[1];
const sample = data[0];
const player = new mm.Player();
mm.Player.tone.Transport.loop = true;
mm.Player.tone.Transport.loopStart = 0;
mm.Player.tone.Transport.loopEnd = 8;
player.start(sample, 120);
}
};
// Add click handler to call the MusicVAE sampling,
// by posting a message to the web worker which
// sample and return the sequence using a message
const buttonSampleMusicVaeTrio = document
.getElementById("button-sample-musicae-trio");
buttonSampleMusicVaeTrio.addEventListener("click", (event) => {
worker.postMessage([]);
event.target.disabled = true;
});
我们已经在前面的示例中覆盖了大部分代码,现在让我们分解新的内容,重点讲解 Web 工作线程的创建以及 Web 工作线程与主线程之间的消息传递,如下所示:
-
首先,我们需要启动工作线程,方法是使用
new Worker("chapter_09_example_05.js")。这将执行 JavaScript 文件的内容并返回一个可以分配给变量的句柄。 -
然后,我们将
onmessage属性绑定到工作线程,当工作线程使用其postMessage函数时,该属性会被调用。在event对象的data属性中,我们可以传递任何我们想要的内容(请参见下面描述的工作线程代码): -
如果工作线程将
initialized作为data数组的第一个元素发送,则意味着工作线程已初始化。 -
如果工作线程将
sample作为data数组的第一个元素发送,则意味着工作线程已对 MusicVAE 序列进行采样,并将其作为data数组的第二个元素返回。 -
最后,当点击 HTML 按钮时,我们在工作线程实例上调用
postMessage方法(无参数,但至少需要一个空数组),这将启动采样过程。
请记住,Web 工作线程与主线程没有共享状态,这意味着所有的数据共享必须通过 onmessage 和 postMessage 方法或函数来实现。
- 现在,让我们编写 JavaScript 工作线程的代码(该代码与 HTML 文件位于同一位置),如下所示:
importScripts("https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@1.4.0/dist/tf.min.js");
importScripts("https://cdn.jsdelivr.net/npm/@magenta/music@¹.12.0/es6/core.js");
importScripts("https://cdn.jsdelivr.net/npm/@magenta/music@¹.12.0/es6/music_vae.js");
async function initialize() {
musicvae = new music_vae.MusicVAE("https://storage.googleapis.com/" +
"magentadata/js/checkpoints/music_vae/trio_4bar");
await musicvae.initialize();
postMessage(["initialized"]);
}
onmessage = function (event) {
Promise.all([musicvae.sample(1)])
.then(samples => postMessage(["sample", samples[0]]));
};
try {
Promise.all([initialize()]);
} catch (error) {
console.error(error);
}
你首先会注意到,我们使用了 Magenta 的 ES6 打包版本,因为我们不能在 Web 工作线程中导入所有内容。例如,导入 Tone.js 时会导致类似 该浏览器不支持 Tone.js 的错误。另外,记住 Magenta.js 尚未完全兼容 Web 工作线程,这意味着导入 GANSynth 可能会导致错误。
由于我们已经在前面的代码块中覆盖了大部分代码,因此我们将只讨论 Web 工作线程的附加内容,如下所示:
-
首先,当模型准备好运行时,我们需要使用
postMessage向主线程发送一个initialized消息。 -
然后,我们绑定在模块的
onmessage属性上,当主线程发送消息给工作线程时,这个属性会被调用。接收到消息后,我们对 MusicVAE 模型进行采样,然后使用postMessage将结果返回给主线程。
这部分涵盖了创建 Web Worker 并使其与主线程交换数据的基本用法。
使用其他 Magenta.js 模型
和往常一样,我们无法涵盖所有模型,但其他模型的使用方法将与我们提供的示例类似。网上有许多 Magenta.js 示例和演示,一些非常令人印象深刻的音乐生成网页应用程序。
我们在 进一步阅读 部分提供了查找示例和演示的资源。
让 Magenta.js 与其他应用程序互动
因为 Magenta.js 运行在浏览器中,它与其他应用程序(如 DAW)的交互要比与 Magenta 应用程序的交互难一些,但随着 Web 标准的发展,这将变得更容易。
使用 Web MIDI API
Web MIDI API (www.w3.org/TR/webmidi/) 是一个 W3C 标准,其规范还不成熟,状态为 W3C 工作草案 2015 年 3 月 17 日。它在浏览器中并不被广泛支持,Firefox 和 Edge 完全不支持。不过,它在 Chrome 中表现得相当不错,因此如果你要求用户使用该浏览器,你的应用可能会正常工作。更多信息请参阅最后一部分 进一步阅读。
你可以在 chapter_08_example_06.html 文件中找到这段代码,它在本章的源代码中。源代码中有更多的注释和内容——你应该去查看一下。
我们将编写一个小示例,使用 Web MIDI API,基于之前的 MusicVAE 示例和三重奏采样。你可以复制之前的示例并添加新内容:
- 首先,让我们向页面中添加一个
select元素,像这样:
<label for="select-midi-output">Select MIDI output:</label>
<select disabled id="select-midi-output">
</select>
- 然后,在
startMusicVae方法中,让我们初始化可用的 MIDI 输出列表,如下所示:
// Starts a MIDI player, and for each available MIDI outputs,
// adds an option to the select drop down.
const player = new mm.MIDIPlayer();
player.requestMIDIAccess()
.then((outputs) => {
if (outputs && outputs.length) {
const option = document.createElement("option");
selectMidiOutput.appendChild(option);
outputs.forEach(output => {
const option = document.createElement("option");
option.innerHTML = output.name;
selectMidiOutput.appendChild(option);
});
selectMidiOutput.disabled = false;
} else {
selectMidiOutput.disabled = true;
}
});
window.player = player;
在这里,我们使用了 Magenta.js 的 MIDIPlayer 类,它使得使用 requestMIDIAccess 方法比直接调用 Web MIDI API 更加简便。调用此方法将返回一个 output 列表,我们通过选择列表中的 name 属性来添加。
- 最后,在
sampleMusicVaeTrio方法中,我们使用播放器将 MIDI 直接发送到该输出,如下所示:
// Gets the selected MIDI output (if any) and uses the
// output in the MIDI player
const midiOutputIndex = selectMidiOutput.selectedIndex;
if (midiOutputIndex) {
player.outputs = [player.availableOutputs[midiOutputIndex - 1]];
mm.Player.tone.Transport.loop = true;
mm.Player.tone.Transport.loopStart = 0;
mm.Player.tone.Transport.loopEnd = 8;
player.start(sample, 120);
}
selectMidiOutput.disabled = true;
在这里,我们只需要用从下拉菜单中选择的元素(如果有)来设置 outputs 列表。
-
为了测试我们的代码,我们可以使用我们可靠的 FluidSynth,使用以下命令:
-
Linux:
fluidsynth -a pulseaudio -g 1 PATH_TO_SF2 -
macOS:
fluidsynth -a coreaudio -g 1 PATH_TO_SF2 -
Windows:
fluidsynth -g 1 PATH_TO_SF2
-
FluidSynth 应该启动并显示一个终端(注意我们移除了 -n 和 -i 标志,以便接收 MIDI 音符)。
- 现在,让我们打开我们的 Web 应用。一旦模型初始化完成,我们应该能够在选择 MIDI 输出的下拉列表中看到 FluidSynth MIDI 输入端口。它应该是这样的:合成器输入端口 (17921:0)。选择这个选项,然后点击Sample MusicVAE trio。你应该能听到来自 FluidSynth 的声音。
你会注意到所有的音符都作为钢琴序列播放,即使我们有三种乐器。这是因为MIDIPlayer非常基础,它不会在鼓道上发送打击乐信号,这是 MIDI 规范中规定的。
在 Node.js 中运行 Magenta.js 的服务器端版本
Magenta.js 也可以在服务器端使用,通过 Node.js 来运行。使用 Node.js 的好处是,你可以在服务器端和客户端运行相同的(或几乎相同的)代码。客户端和服务器之间的通信可以通过 WebSockets 来处理。
WebSocket API(developer.mozilla.org/en-US/docs/…)是一个 API,它使得客户端和服务器之间能够进行双向通信。我们在这里不会详细讨论 WebSockets,但它们可以是一个非常好的方式,用于在服务器端 Magenta 进程(使用 Node.js 的 Magenta.js 或 Python 中的 Magenta)和客户端应用程序之间传输数据。使用 WebSockets 最简单的方式是使用像 Socket.IO 这样的框架(socket.io/)。
使用 Node.js 的另一个优点是我们的程序在服务器端运行,这意味着它不依赖于浏览器的实现。一个很好的例子是,我们可以使用 Node.js 的包来处理向其他进程发送 MIDI,例如node-midi(www.npmjs.com/package/mid…),这样就不需要使用 Web MIDI API。
让我们展示一个 Magenta.js 与 Node.js 一起运行的简单示例。这里显示的代码与我们之前在 JavaScript 中讨论的内容类似:
你可以在本章的源代码中的chapter_08_example_07.js文件中找到这段代码。源代码中有更多的注释和内容——你应该去查看一下。
-
首先,让我们安装 Node.js(nodejs.org/en/download…)
-
接下来,让我们使用
npm命令安装 Magenta.js,npm是 Node.js 的依赖管理工具,命令如下:
npm install --save @magenta/music
这将把 Magenta.js 及其依赖项安装到node_modules目录中。当 Node.js 运行时,它会在这个目录中查找脚本的依赖项,处理每个require调用。
- 现在我们可以创建一个 JavaScript 文件来采样一个序列,如下所示:
const music_vae = require("@magenta/music/node/music_vae");
// These hacks below are needed because the library uses performance
// and fetch which exist in browsers but not in node.
const globalAny = global;
globalAny.performance = Date;
globalAny.fetch = require("node-fetch");
const model = new music_vae.MusicVAE(
"https://storage.googleapis.com/magentadata/js/checkpoints/" +
"music_vae/drums_2bar_lokl_small");
model
.initialize()
.then(() => model.sample(1))
.then(samples => {
console.log(samples[0])
});
这段代码与之前的示例相似,唯一的区别是添加了require方法,该方法在 Node.js 中用于导入依赖模块。
- 要执行你的 Node.js 应用,请使用
node命令(将PATH_TO_JAVASCRIPT_FILE替换为合适的值),如以下所示:
node PATH_TO_JAVASCRIPT_FILE
由于我们使用了 console.log,样本应该显示在控制台上。你还会注意到控制台上有一些信息,内容如下:
This browser does not support Tone.js
Hi there. Looks like you are running TensorFlow.js in Node.js. To speed things up dramatically, install our node backend, which binds to TensorFlow C++, by running npm i @tensorflow/tfjs-node, or npm i @tensorflow/tfjs-node-gpu if you have CUDA. Then call require('@tensorflow/tfjs-node'); (-gpu suffix for CUDA) at the start of your program. Visit https://github.com/tensorflow/tfjs-node for more details.
这提醒我们,Tone.js 不能在 Node.js 上运行,因为 Web Audio API 是在客户端实现的。它还提醒我们,Node.js 可以使用 CUDA 库来提升性能。
总结
在这一章中,我们介绍了 Tensorflow.js 和 Magenta.js,分别是 TensorFlow 和 Magenta 的 JavaScript 实现。我们了解到 TensorFlow.js 是通过 WebGL 加速的,并且 Magenta.js 仅提供有限的模型集,这些模型只能用于生成,不能用于训练。我们还将上一章中使用 Python 训练的模型转换为 TensorFlow.js 可加载的格式。我们还介绍了 Tone.js 和 Web Audio API,Magenta.js 使用它们在浏览器中合成声音。
然后,我们创建了三个音乐生成 web 应用程序。第一个应用使用 GANSynth 采样短音频音符。通过这样做,我们学会了如何导入所需的脚本,可以使用一个大的 ES5 包,也可以使用一个更小的、拆分的 ES6 包。第二个应用使用 MusicVAE 采样三件乐器,分别是鼓组、贝斯和主旋律,并循环播放该序列。第三个应用同时使用了这两个模型生成序列和音频,并介绍了如何使用 Web Workers API 将计算任务转移到另一个线程。
最后,我们讨论了如何使 Magenta.js 与其他应用程序进行交互。我们使用 Web MIDI API 将生成的序列发送到另一个合成器——例如,FluidSynth。我们还使用了 Node.js 在服务器端运行 Magenta.js 应用程序。
Magenta.js 是一个很棒的项目,因为它使得使用 web 技术创建和分享音乐生成应用程序变得容易。还有其他方法可以将 Magenta 融入更广泛的应用场景,例如使用 Magenta Studio(它可以让 Magenta 在 Ableton Live 中运行)和使用 MIDI,这是控制各种设备(如软件和硬件合成器)的好方法。我们将在下一章中展示这些内容。
问题
-
是否可以使用 Tensorflow.js 来训练模型?使用 Magenta.js 可以吗?
-
Web Audio API 的作用是什么,使用它的最简单方法是什么?
-
GANSynth 中的生成方法是什么?需要提供什么参数?
-
MusicVAE 中的生成方法是什么?它生成多少种乐器?
-
为什么 Web Workers API 在 JavaScript 中有用?
-
列举两种将 MIDI 从 Magenta.js 应用程序发送到另一个应用程序的方法。
进一步阅读
-
MagentaMusic.js 演示:Magenta 维护的演示列表,展示了如何使用 Magenta.js 中的各种模型和核心类 (tensorflow.github.io/magenta-js/…)。
-
使用 Magenta.js 构建的 Web 应用:一个由社区推动的 Magenta.js 演示列表,包含许多酷炫的内容(magenta.tensorflow.org/demos/web/)。
-
Monica Dinculescu——为什么你应该构建一些傻乎乎的东西:关于 Magenta.js 的重要性以及分享音乐创作应用的有趣演讲(www.youtube.com/watch?v=Dki…)。
-
庆祝约翰·塞巴斯蒂安·巴赫:一个流行的音乐生成应用的好例子(www.google.com/doodles/cel…)。
-
WebGL 规范:WebGL 规范(www.khronos.org/registry/we…)。
-
平台与环境:关于在 TensorFlow.js 中使用 WebGL 进行内存管理和 GPU 计算的有趣读物(www.tensorflow.org/js/guide/pl…)。
-
Web Audio API:来自 W3C 的 Web Audio API 规范(webaudio.github.io/web-audio-a…)。
-
Web Audio API:Web Audio API 的介绍(developer.mozilla.org/en-US/docs/…)。
-
Web Workers:来自 WHATWG 的 Web Workers API 规范(html.spec.whatwg.org/multipage/w…)。
-
并发模型与事件循环:JavaScript 中事件循环模式的介绍(developer.mozilla.org/en-US/docs/…)。
-
使用 Web Workers:Web Workers API 的介绍(developer.mozilla.org/en-US/docs/…)。
-
Web MIDI API:W3C 的 Web MIDI API 规范(webaudio.github.io/web-midi-ap…)。
-
Web MIDI(Web 浏览器中的 MIDI 支持):来自 MIDI 协会的 Web MIDI API 介绍,带有使用示例的应用程序(www.midi.org/17-the-mma/…)。
第十一章:使 Magenta 与音乐应用程序互动
在本章中,我们将展示 Magenta 如何融入更广泛的应用场景,展示如何使其与其他音乐应用程序(如数字音频工作站(DAWs)和合成器)互动。我们将解释如何通过 MIDI 接口将 MIDI 序列从 Magenta 发送到 FluidSynth 和 DAW。通过这种方式,我们将学习如何在所有平台上处理 MIDI 端口,以及如何在 Magenta 中循环 MIDI 序列。我们还将展示如何使用 MIDI 时钟和传输信息同步多个应用程序。最后,我们将介绍 Magenta Studio,它是基于 Magenta.js 的 Magenta 独立包装,可以作为插件集成到 Ableton Live 中。
本章将涵盖以下主题:
-
将 MIDI 发送到 DAW 或合成器
-
循环生成的 MIDI
-
将 Magenta 作为独立应用程序与 Magenta Studio 一起使用
技术要求
在本章中,我们将使用以下工具:
-
使用命令行或Bash从终端启动 Magenta
-
使用Python及其库编写音乐生成代码,使用 Magenta
-
使用Magenta生成 MIDI 音乐并与其他应用程序同步
-
使用Mido及其他 MIDI 工具发送 MIDI 音符和时钟
-
使用FluidSynth接收来自 Magenta 的 MIDI
-
你选择的DAW(例如 Ableton Live、Bitwig 等)来接收来自 Magenta 的 MIDI
-
Magenta Studio作为独立应用程序或 Ableton Live 插件
在 Magenta 中,我们将使用MIDI 接口将 MIDI 序列和 MIDI 时钟发送到其他音乐应用程序。我们将深入讨论其使用方法,但如果你需要更多信息,可以查看 Magenta 源代码中的 Magenta MIDI 接口README.md,链接为(github.com/tensorflow/…),这是一个很好的起点。你还可以查看 Magenta 的代码,里面有很好的文档。我们还在本章末尾的进一步阅读部分提供了额外的内容。
我们还将使用Magenta Studio项目,你可以在其 GitHub 页面上找到更多信息,链接为github.com/tensorflow/…。
本章的代码位于本书 GitHub 仓库中的Chapter09文件夹,地址为github.com/PacktPublis…。示例和代码片段假定你位于该章节的文件夹中。开始之前,你应使用cd Chapter09进入该文件夹。查看以下视频,了解代码的实际操作:bit.ly/2RGkEaG。
将 MIDI 发送到 DAW 或合成器
从本书开始,我们一直在生成 MIDI 作为物理文件,然后使用 MuseScore 或 FluidSynth 进行播放。这是一种很好的作曲方式,能够生成新的序列,保留我们喜欢的部分,并基于它们生成更多内容。但是,如果我们希望 MIDI 音符在模型生成它们时持续播放呢?这就是建立一个自主的音乐生成系统的好方法,Magenta 作为作曲者,外部程序作为播放器,播放它收到的音符并使用乐器。
在这一部分,我们将介绍如何将 MIDI 从 Magenta 发送到合成器或 DAW。我们还将展示如何循环 Magenta 生成的序列,并如何将我们的 Magenta 程序与发送序列的应用程序同步。
介绍一些 DAW
使用 DAW 制作音乐相较于简单的合成器(如 FluidSynth)有很多优势:
-
录制和编辑 MIDI序列
-
录制和编辑音频,无论是母带轨道还是单一(乐器)轨道
-
使用振荡器、包络、滤波器等创建我们自己的合成器
-
使用效果(如混响、延迟、饱和度等)
-
对音轨应用均衡器(EQ)和母带处理(mastering)
-
剪辑、合并和混合音频片段以制作完整的轨道
市面上有很多 DAW,但不幸的是,其中很少有开源或免费的可供使用。我们将对一些我们认为与 Magenta 搭配使用时很有趣的 DAW 进行简要介绍(这并不是全面的介绍):
-
Ableton Live(www.ableton.com – 非免费)是音乐行业中广为人知的产品,已有很长历史。Ableton Live 是市场上最完整的 DAW 之一,但其所有功能的价格较高。它仅支持 Windows 和 macOS 系统。
-
Bitwig(www.bitwig.com – 非免费)也是一款非常完整的产品,类似于 Ableton Live,价格略低于其对手。它是一款功能丰富的 DAW,支持所有平台:Windows、macOS 和 Linux。
-
Reason(www.reasonstudios.com/ – 非免费)是一款专注于乐器和效果的 DAW,而非作曲。它与其他软件(如 Ableton Live 或 Magenta)结合使用时,MIDI 编排效果特别好。它仅支持 Windows 和 macOS 系统。
-
Cubase(new.steinberg.net/cubase/ – 非免费),由著名的音频软件和硬件公司 Steinberg 开发,是市场上最古老的 DAW 之一。它仅支持 Windows 和 macOS 系统。
-
Cakewalk(www.bandlab.com/products/ca… – 免费)是 Bandlab 推出的一个完整且易于使用的 DAW。这是唯一一款非开源但免费的 DAW。遗憾的是,它仅支持 Windows 系统。
-
SuperCollider (supercollider.github.io/ – 免费且开源) 是一个音频合成和算法作曲的平台,允许通过编程开发合成器和效果器,使用的编程语言叫做
sclang。它适用于所有平台并且是开源的。 -
VCV Rack (vcvrack.com/ – 免费且开源) 是一个 DAW,它以软件形式再现了模块化合成的乐趣。它适用于所有平台并且是开源的。
我们将使用 Ableton Live 来做示例,但所有 DAW 在接收 MIDI 方面都有类似的功能,所以这些示例应该适用于所有软件。如果有必要,我们会重点说明一些注意事项,比如在 Linux 上处理 MIDI 路由的问题。
使用 Mido 查看 MIDI 端口
首先,我们需要查找机器上可用的 MIDI 端口(如果有的话),以便在应用程序之间发送 MIDI 信息,比如从 Magenta 到 FluidSynth 或 DAW。这里有一个非常实用的库叫做 Mido,它是 Python 的 MIDI 对象库(mido.readthedocs.io),在查找 MIDI 端口、创建新端口和发送数据方面非常有用。
由于 Magenta 依赖于 Mido,它已经在我们的 Magenta 环境中安装好了。
你可以在本章的源代码中的chapter_09_example_01.py文件中跟随这个示例。源代码中有更多的注释和内容,所以你应该去查看一下。
让我们来看一下我们机器上可用的 MIDI 端口:
import mido
print(f"Input ports: {mido.get_input_names()}")
print(f"Output ports: {mido.get_output_names()}")
这应该会产生类似于以下的输出:
Input ports: ['Midi Through:Midi Through Port-0 14:0']
Output ports: ['Midi Through:Midi Through Port-0 14:0']
在 Linux 和 macOS 上,应该已经有一个输入端口和一个输出端口,如前面的输出所示。在 Windows 上,列表可能是空的,因为操作系统不会自动创建任何虚拟 MIDI 端口,或者列表只包含Microsoft GS Wavetable Synth,这是一个类似 FluidSynth 的 MIDI 合成器。
让我们来看一下如何为我们的应用程序创建新的端口以进行通信。
在 macOS 和 Linux 上创建虚拟 MIDI 端口
FluidSynth 的一个优点是它在启动时会自动打开一个虚拟 MIDI 端口。不幸的是,它在 Windows 上无法使用,因此我们首先会了解如何创建虚拟 MIDI 端口。
虚拟 MIDI 端口是可以创建的 MIDI 端口,用于应用程序之间发送 MIDI 消息。这是所有音乐制作应用程序的必备功能。为了让 Magenta 将 MIDI 数据发送到其他程序(如 DAW),我们需要为它们打开一个虚拟端口以便进行通信。
正如我们在前一个例子中看到的,虚拟 MIDI 端口分为输入端口和输出端口。这意味着我们可以创建一个名为magenta的输入端口和一个名为magenta的输出端口。通常来说,使用两个不同的名称会更清晰,例如,magenta_out用于输出端口,magenta_in用于输入端口。在 DAW 中映射端口时,这样也会更简单。
我们将从 Magenta 的角度选择端口名称,也就是说,magenta_out之所以被命名为magenta_out,是因为 Magenta 正在发送信息。
在 macOS 和 Linux 上,创建新的虚拟端口非常简单,因为 Mido 支持可以创建端口的 RtMidi 后台。在 Magenta 中使用MidiHub,我们可以为每个输入和输出提供一个字符串,表示我们想要创建的虚拟端口名称:
from magenta.interfaces.midi.midi_hub import MidiHub
# Doesn't work on Windows if the ports do not exist
midi_hub = MidiHub(input_midi_ports="magenta_in",
output_midi_ports="magenta_out",
texture_type=None)
如果端口不存在,这将创建两个虚拟端口,magenta_in和magenta_out,如果已存在,则使用现有的。仅使用 Mido,我们可以使用以下代码:
import mido
# Doesn't work on Windows if the ports do not exist
inport = mido.open_input("magenta_in")
outport = mido.open_output("magenta_out")
请注意,输入端口有一个receive方法,而输出端口有一个send方法。当打印端口时,我们应该看到以下内容:
Input ports: ['Midi Through:Midi Through Port-0 14:0', 'RtMidiOut Client:magenta_out 128:0']
Output ports: ['Midi Through:Midi Through Port-0 14:0', 'RtMidiIn Client:magenta_in 128:0']
现在,命名的虚拟端口可以在重启前供应用程序使用。
然而,具体是否有效取决于所使用的 DAW。例如,Linux 下的 Bitwig 与 ALSA 虚拟端口配合不佳,因此仅仅通过 RtMidi 打开一个端口是不够的;你需要查看文档,寻找使用JACK 音频连接工具包(JACK)的解决方法。其他 Linux 上的 DAW,例如 VCV Rack,则能正常工作并显示虚拟端口。
使用 loopMIDI 在 Windows 上创建虚拟 MIDI 端口
在 Windows 上,我们无法使用之前提供的代码创建虚拟端口。幸运的是,我们有loopMIDI软件(www.tobias-erichsen.de/software/lo…),这是一款小而老的程序,在 Windows 上使用 MIDI 时简直是救星。它唯一的功能就是在机器上创建命名的虚拟 MIDI 端口。
安装完成后,启动软件,并使用底部的名称字段和加号按钮创建两个新的端口,命名为magenta_in和magenta_out:
命名为magenta_in和magenta_out的虚拟端口现在应该可以同时用于 Ableton Live 和 Magenta 进行通信。当创建新端口时,loopMIDI总是同时创建输入端口和输出端口,这意味着我们可以从magenta_in端口发送和接收 MIDI。为了简便起见,我们将保持两个端口分开。
在 Windows 上,如果启动 Magenta MidiHub时遇到以下错误,那是因为你没有正确创建或命名虚拟端口:
INFO:tensorflow:Opening '['magenta_out 2']' as a virtual MIDI port for output.
I1218 15:05:52.208604 6012 midi_hub.py:932] Opening '['magenta_out 2']' as a virtual MIDI port for output.
Traceback (most recent call last):
...
NotImplementedError: Virtual ports are not supported by the Windows MultiMedia API.
请注意端口名称magenta_out 2中也包含了端口索引2。这在 Windows 中引用端口时非常重要,因为它们是使用格式:名称 索引进行命名的。这有点麻烦,因为如果你创建新的端口(或插件新的 MIDI 设备)来改变索引,端口索引可能会发生变化。
为了解决这个问题,我们确保使用字符串包含而不是精确匹配来过滤端口(我们提供的所有示例在这方面都能正常工作)。
在 macOS 上添加虚拟 MIDI 端口
在 macOS 上,我们可以使用前面在查看虚拟 MIDI 端口部分中描述的那种方法,或者使用内置的 macOS 界面创建一个新的虚拟端口。使用内置界面很简单:
-
启动音频 MIDI 设置。
-
打开窗口菜单并点击显示 MIDI 工作室。
-
选择IAC 驱动程序图标。
-
启用设备在线复选框。
然后,我们可以使用**+**按钮创建命名的虚拟端口。
发送生成的 MIDI 到 FluidSynth
为了将 Magenta 生成的 MIDI 发送到 FluidSynth,我们将从第二章中编写的第一个示例中,使用 DrumsRNN 生成鼓序列,并添加一些代码将 MIDI 消息直接发送到软件合成器。
你可以在本章的源代码中找到chapter_09_example_02.py文件中的示例。源代码中有更多的注释和内容,所以你应该去查看一下。
这与我们在上一章中使用 Web MIDI API 从浏览器将 MIDI 音符发送到 FluidSynth 时所做的类似:
-
首先,我们将使用以下之一启动 FluidSynth:
-
Linux:
fluidsynth -a pulseaudio -g 1 PATH_TO_SF2 -
macOS:
fluidsynth -a coreaudio -g 1 PATH_TO_SF2 -
Windows:
fluidsynth -g 1 -o midi.winmidi.device=magenta_out PATH_TO_SF2
-
请注意 Windows 命令中的-o标志,它告诉 FluidSynth 监听这个 MIDI 端口,因为在 Windows 上,它不会自动打开端口。
另外,请注意我们这次没有使用-n和-i标志,因为我们希望保留传入的 MIDI 消息并使用合成器命令行。程序应该会停留在命令行界面,并且应该自动创建一个新的输入 MIDI 端口(或者使用提供的端口)。
在 Windows 上,如果你在启动 FluidSynth 时看到以下错误消息:fluidsynth: error: no MIDI in devices found 或 Failed to create the MIDI thread,这意味着你可能拼写错误了 MIDI 端口名,或者没有打开loopMIDI。
在 macOS 和 Linux 上,你可以再次运行之前的示例代码,应该会看到类似如下的输出:
Input ports: ['Midi Through:Midi Through Port-0 14:0', 'RtMidiOut Client:magenta_out 128:0']
Output ports: ['FLUID Synth (7171):Synth input port (7171:0) 129:0', 'Midi Through:Midi Through Port-0 14:0', 'RtMidiIn Client:magenta_in 128:0']
在这里,FLUID Synth (7171): Synth input port (7171:0) 129:0端口是 FluidSynth 端口。我们还可以看到来自前一个示例的magenta_out和magenta_in端口。
在 Windows 上,重新运行之前的示例代码应该会给你这个:
Input ports: ['magenta_in 0', 'magenta_out 1']
Output ports: ['Microsoft GS Wavetable Synth 1', 'magenta_in 2', 'magenta_out 3']
我们将使用的 FluidSynth 输入端口是magenta_out 3端口,它应该与提供给 FluidSynth 的-o midi.winmidi.device=magenta_out标志匹配。
- 接下来,我们将复制
chapter_02_example_01.py示例:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--midi_port", type=str, default="FLUID Synth")
args = parser.parse_args()
def generate(unused_argv):
# The previous example is here
...
# Write the resulting plot file to the output directory
plot_file = os.path.join("output", "out.html")
pretty_midi = mm.midi_io.note_sequence_to_pretty_midi(sequence)
plotter = Plotter()
plotter.show(pretty_midi, plot_file)
print(f"Generated plot file: {os.path.abspath(plot_file)}")
# Write the code to send the generated "sequence" to FluidSynth
pass
return 0
if __name__ == "__main__":
tf.app.run(generate)
我们添加了一个--midi_port标志来轻松更改 MIDI 输出端口(记住,输入和输出术语是从 Magenta 的角度看待的)。我们将在generate方法的末尾编写代码,以发送 MIDI 内容(它存储在sequence变量中)。
- 我们找到提供的输出端口并使用该端口初始化
MidiHub:
import mido
from magenta.interfaces.midi.midi_hub import MidiHub
# We find the proper input port for the software synth
# (which is the output port for Magenta)
output_ports = [name for name in mido.get_output_names()
if args.midi_port in name]
# Start a new MIDI hub on that port (output only)
midi_hub = MidiHub(input_midi_ports=[],
output_midi_ports=output_ports,
texture_type=None)
然后,我们在该端口上启动一个新的 MIDI 中心;它将作为我们应用程序与合成器之间的通信接口。它很有用,因为它使我们能够直接使用NoteSequence对象,而无需手动转换它们。
midi_hub模块位于 Magenta 的magenta.interfaces.midi模块中,并包含处理 MIDI 的有用工具。
- 接下来,我们将从中心获取一个播放器实例,并将播放通道设置为
9:
import music_pb2
empty_sequence = music_pb2.NoteSequence()
player = midi_hub.start_playback(empty_sequence, allow_updates=True)
player._channel = 9
请记住,兼容 GM 1 的合成器如果 MIDI 通道为10时会播放鼓声音色(但在 Magenta MIDI 中,通道是从零开始计数的,因此我们需要使用9)。我们将在一个空序列上开始播放,允许稍后更新序列。
- 现在我们可以播放我们的
sequence,但首先需要调整它,以便播放器知道何时开始:
import time
from magenta.interfaces.midi.midi_interaction import adjust_sequence_times
wall_start_time = time.time()
sequence_adjusted = music_pb2.NoteSequence()
sequence_adjusted.CopyFrom(sequence)
sequence_adjusted = adjust_sequence_times(sequence_adjusted,
wall_start_time)
MIDI 播放器将根据墙时(wall time)播放sequence,但我们的序列从0开始(墙时是从纪元开始的时间)。例如,如果墙时(由time.time()提供)为1564950205,那么我们需要将序列的起始时间向前调整这个数值。我们通过保持当前序列不变,并制作一个副本交给播放器来做到这一点。我们使用 Magenta 中的adjust_sequence_times函数来完成这个操作。
请注意这里使用了CopyFrom方法,该方法存在于 Protobuf 消息对象中。你可以随时检查google.protobuf.message.Message类中的方法,以便找到对NoteSequence有用的方法。
- 现在我们已经将序列调整到正确的时间,让我们播放它吧!我们使用播放器上的
update_sequence方法来实现这一点,它相当于play:
player.update_sequence(sequence_adjusted, start_time=wall_start_time)
try:
player.join(generation_end_time)
except KeyboardInterrupt:
return 0
finally:
return 0
我们还向播放器的instance提供了start_time参数,这个参数等于我们调整后的(向前偏移的)序列的起始时间。
由于player是一个线程,我们需要等它完成后再退出,否则程序会在序列播放之前退出。我们通过在播放器实例上使用join方法来做到这一点,join方法存在于任何线程类中。这个方法会阻塞,直到线程完成,但因为播放器线程永远不会停止,这个调用将无限期阻塞。通过添加generation_end_time(即生成序列的长度)作为超时,这个调用将在序列播放结束后返回。被阻塞的join调用可以通过按Ctrl + C中断,此操作会被KeyboardInterrupt异常类捕获。
- 现在,我们可以在 Linux 和 macOS 上使用以下命令启动程序:
> python chapter_09_example_02.py
通过保持默认的--midi_port标志,它将使用 FluidSynth 启动的端口。
或者我们可以在 Windows 上使用magenta_out MIDI 端口:
> python chapter_09_example_02.py --midi_port=magenta_out
现在,你应该能听到你的音乐从 FluidSynth 播放!在执行代码时,你可能会看到以下警告:
WARNING:tensorflow:No input port specified. Capture disabled.
这是因为 MIDI 中心(MIDI hub)也可以接收 MIDI 消息,但我们尚未提供任何 MIDI 端口来接收。因此,这仅仅是一个警告,不应该成为问题。
将生成的 MIDI 发送到 DAW
将 MIDI 发送到 FluidSynth 很不错,但你可能希望使用其他软件来制作音乐。我们不会讨论所有 DAW,但会展示一些适用于大多数音乐制作软件的示例。
现在我们已经为从 Magenta 应用程序传输 MIDI 打开了虚拟 MIDI 端口,接下来在 Ableton Live 中进行测试。你也可以在任何其他具备 MIDI 功能的 DAW 中尝试此方法。
你可以在本章的源代码中找到 Ableton Live 设置(扩展名为 .als 文件),路径为 chapter_09_example_02.als 文件。
你可以将这个 Ableton 设置与我们在前一个示例中展示的 Python 代码 chapter_09_example_02.py 一起使用。
让我们在 Ableton Live 中配置 magenta_out 端口,该端口也将被 Magenta 应用程序使用:
- 首先,在 Ableton 中,进入 文件 > 选项 > 首选项... > 链接 MIDI,然后找到
magenta_out输入:
我们需要将 轨道 和 远程 都设置为 开启 以接收 MIDI 音符。
-
现在 MIDI 输入已被激活,我们可以通过右键点击 在此处拖放文件和设备 区域,选择 插入 MIDI 轨道 来创建一个新的 MIDI 轨道。
-
在新轨道中,我们可以看到以下的 MIDI From 区域:
在截图中,我们标出了三个部分:
-
在 MIDI From 区域,这是一个 MIDI 轨道的设置,我们现在可以选择
magenta_outMIDI 端口。我们还选择了 Ch. 10 作为鼓道 10 和 监视器 设置为 输入。 -
第三八度 位于表示所有 127 种可能 MIDI 值的 8 个八度音阶条上,其中定义了 808 核心套件。这对应于 MIDI 音符 36 到 52,你可以看到音符 38 当前正在播放。
-
当前播放的音符,808 小军鼓,属于 808 核心套件 乐器。
在右上角,一个黄色指示灯显示是否有输入 MIDI,这对于调试非常有用。
- 现在我们已经设置好了 Ableton Live,可以通过以下方式启动我们的应用程序:
> python chapter_09_example_02.py --midi_port="magenta_out"
你应该能在 Ableton Live 中接收到 MIDI 信号,并听到 808 核心套件 播放打击乐音效。
使用 NSynth 生成的样本作为乐器
在前一章节 第五章,使用 NSynth 和 GANSynth 生成音频,我们讨论了如何通过使用 Magenta 生成的 MIDI 来编排我们生成的样本。现在我们可以动态地将生成的 MIDI 发送到 DAW,这正是一个很好的测试时机。
在 Ableton Live 中,在 808 核心套件 区域,我们可以拖放一个生成的样本来替换现有的鼓组样本。例如,我们可以将 Cowbell 808 乐器替换为我们的一个样本,例如 160045_412017:
当双击新声音时,采样器界面将打开,你可以修改循环的开始和结束位置,以及音量。我们选择这个样本是因为它有很强的攻击性(声音包络上升得很快),非常适合做打击乐样本。你也可以尝试自己的样本。
在映射通道 10 上的鼓声时,请记住打击乐器是根据 MIDI 音高选择的。在之前的图中,网格中的 16 种乐器被映射到 MIDI 音高,如下所示:
| 48 | 49 | 50 | 51 |
|---|---|---|---|
| 44 | 45 | 46 | 47 |
| 40 | 41 | 42 | 43 |
| 36 | 37 | 38 | 39 |
在这里,音高 36 对应于Kick 808,音高 37 对应于Rim 808,音高 51 对应于我们的160045_412017样本,依此类推。你可以将这个网格与我们的程序输出的 MIDI 图(在output/out.html中)进行对比。
这对于鼓元素非常有效。但如果你将旋律发送到 DAW,你可能会想使用采样器,它会根据输入音符改变声音的音高。为此,在 Ableton Live 中,按照以下步骤操作:
-
右键点击Drop Files and Devices Here区域,选择Insert MIDI track来创建一个新的 MIDI 轨道。
-
通过选择Instruments > Sampler来找到Sampler乐器。
-
将Sampler拖放到底部的Drop Audio Effects Here区域(在新的 MIDI 轨道中)。
-
将生成的
412017_83249样本(或你选择的其他样本)拖放到底部的Drop Sample Here区域(在Sampler中)。
我们选择了412017_83249生成的样本,因为猫的声音在作为旋律播放时发出一个不错的(且有趣的)音符。你应该看到以下界面:
现在,当你从 Magenta 程序发送旋律时,你会听到样本412017_83249被播放并根据旋律音符的音高进行升降调。
循环生成的 MIDI
现在我们可以将生成的 MIDI 发送到 DAW,让我们来看一下如何循环生成的 MIDI。这开启了许多不同的用例,例如构建一个持续生成音乐的系统。我们将首先看看如何循环NoteSequence。我们还将讨论如何使用 MIDI 时钟将 Magenta 与 DAW 同步,这在长时间运行的现场音乐系统中非常重要。
使用 MIDI 播放器循环一个序列
在这个示例中,我们将使用 Magenta 中的player实例来循环生成的NoteSequence,通过复制序列并在稍后的时间播放,直到播放器结束播放。
你可以在本章源代码中的chapter_09_example_03.py文件中跟随这个示例。源代码中有更多注释和内容,所以你应该去查看。
让我们用之前的例子并让序列无限循环:
- 首先,我们来找出周期,这相当于循环时间(以秒为单位):
from decimal import Decimal
from magenta.common import concurrency
period = Decimal(240) / qpm
period = period * (num_bars + 1)
sleeper = concurrency.Sleeper()
在这里,我们需要一个 4 小节的周期(以秒为单位),即循环长度。使用 240/QPM,我们可以得到 1 小节的周期(例如,120 QPM 下为 2 秒)。然后我们将其乘以 4 小节(num_bars + 1),这就是我们的循环长度。此外,我们使用Decimal类,它不像内置的float那样有舍入误差,以提高时间精度。
我们利用 Magenta 的Sleeper类,它实现了比time模块中的sleep更精确的版本,因此它应该能以正确的时间更加一致地唤醒。
- 现在让我们定义主循环,它将复制当前序列,调整时间并使用播放器播放:
while True:
try:
# We get the next tick time by using the period
# to find the absolute tick number (since epoch)
now = Decimal(time.time())
tick_number = int(now // period)
tick_number_next = tick_number + 1
tick_time = tick_number * period
tick_time_next = tick_number_next * period
# Update the player time to the current tick time
sequence_adjusted = music_pb2.NoteSequence()
sequence_adjusted.CopyFrom(sequence)
sequence_adjusted = adjust_sequence_times(sequence_adjusted,
float(tick_time))
player.update_sequence(sequence_adjusted,
start_time=float(tick_time))
# Sleep until the next tick time
sleeper.sleep_until(float(tick_time_next))
except KeyboardInterrupt:
print(f"Stopping")
return 0
让我们稍微解析一下代码:
-
在每个循环开始时,我们获取当前的自纪元以来的时间(以
now表示)。 -
我们通过将当前时间除以周期来获取当前的节拍数(以
tick_number表示)。节拍数对应于从纪元到现在的时间区间按period分割后的当前索引。 -
我们通过将周期与节拍数相乘来获取当前的节拍时间(以
tick_time表示)。
例如,如果起始时间是1577021349,我们有一个滴答时间1577021344和下一个滴答时间1577021352(周期为 8 秒)。在这种情况下,我们处于循环的第一次迭代,这就是为什么起始时间和滴答时间之间有如此大的差异。第二次循环时,起始时间将是1577021352(大约),因为线程将在正确的时间唤醒。
由于第一次循环的起始时间差异,这意味着当播放器启动时,它可能会从生成的序列的中间开始。如果我们希望它从序列的开头开始,我们需要在计算节拍数时减去起始时间。请查看magenta.interfaces.midi.midi_hub模块中的Metronome类,了解更完整的实现。
最后,我们使用tick_time更新序列和播放器,并在tick_time_next之前休眠。
- 现在我们可以通过以下方式启动程序:
> python chapter_09_example_03.py --midi_port="magenta_out"
你现在应该能在你使用的 DAW 中听到一个 120 QPM、持续 8 秒的 4 小节循环。
将 Magenta 与 DAW 同步
在演奏乐器时,同步设备非常重要。两个同步的乐器会有相同的 QPM(节奏)并且在相同的拍子(相位)上开始。解决这些问题表面看起来很简单,但良好的同步非常难以实现,因为精确的时间控制很困难。
将我们的 Magenta 应用与 DAW 同步有很多用途,例如,在 DAW 中以正确的时间(节奏和相位)录制 MIDI 序列,或者同时播放多个序列,其中一些来自 Magenta,另一些来自 DAW。
发送 MIDI 时钟和传输
在此示例中,我们将使用 MIDI 时钟和传输(启动、停止和重置)信息将 Magenta 与数字音频工作站(DAW)同步。MIDI 时钟是最古老且最流行的设备同步方式之一,几乎所有的乐器和音乐软件都支持它。
我们将给出在 Ableton Live 中的示例,但你也可以在任何具有 MIDI 时钟功能的 DAW 中尝试此操作。
你可以在本章的源代码中的chapter_09_example_04.py文件中查看此示例。源代码中有更多的注释和内容,你应该去查看一下。
为了将我们的 Magenta 程序与 Ableton Live 同步,我们将启动一个节拍器线程,该线程将在每个节拍上唤醒并发送一个时钟消息:
- 首先,让我们声明
Metronome类,它继承自Thread类:
import mido
from decimal import Decimal
from threading import Thread
class Metronome(Thread):
def __init__(self, outport, qpm):
super(Metronome, self).__init__()
self._message_clock = mido.Message(type='clock')
self._message_start = mido.Message(type='start')
self._message_stop = mido.Message(type='stop')
self._message_reset = mido.Message(type='reset')
self._outport = outport
self._period = Decimal(2.5) / qpm
self._stop_signal = False
def stop(self):
self._stop_signal = True
def run(self):
# Run code
pass
在实例化时,我们使用 Mido 定义以下消息(有关 Mido 支持的消息及其在 MIDI 规范中的对应项,请参阅最后一节,进一步阅读):
-
-
clock消息,每个节拍发送一次 -
start消息,在序列开始时发送 -
stop消息,在序列结束时或程序退出时发送 -
reset消息,在start消息之前发送,确保同步的设备从节拍计数的开始重新启动 -
continue消息,我们不会使用它,但它可以用来在不重置节拍计数的情况下重新启动播放
-
我们还定义了周期,即每次线程唤醒之间的确切时间。线程需要在每个节拍时唤醒,因此在 120 QPM 的 4/4 拍中,它需要每 0.5 秒唤醒一次,这就是周期。
在这里,我们选择使用每个节拍一个消息(或脉冲)来同步两个应用程序,这是我们的周期,因为这样做很简单。在 MIDI 规范中(www.midi.org/specificati…),还描述了另一种同步周期,称为24 每四分之一音符脉冲(24 PPQN),它比我们这里实现的更精确。
每个节拍一个脉冲和 24 PPQN 都在许多 DAW 和乐器中使用。然而,还有其他的同步脉冲,例如 Korg 乐器使用的 48 PPQN。还有其他同步乐器的方式,例如MIDI 时间码(MTC),我们在这里不讨论。
根据你尝试同步的软件或硬件,确保检查它们配置处理的同步脉冲类型。如果这个方法不起作用,可能是因为你发送了一个意外的脉冲率。
- 现在,让我们实现
# Run code注释中的run方法:
import time
from magenta.common.concurrency import Sleeper
def run(self):
sleeper = Sleeper()
# Sends reset and the start, we could also
# use the "continue" message
self._outport.send(self._message_reset)
self._outport.send(self._message_start)
# Loops until the stop signal is True
while not self._stop_signal:
# Calculates the next tick for current time
now = Decimal(time.time())
tick_number = max(0, int(now // self._period) + 1)
tick_time = tick_number * self._period
sleeper.sleep_until(float(tick_time))
# Sends the clock message as soon it wakeup
self._outport.send(self._message_clock)
# Sends a stop message when finished
self._outport.send(self._message_stop)
以下列表进一步解释了代码:
-
-
当线程首次启动时,它发送一个
reset消息,紧接着发送一个start消息,意味着 Ableton Live 将其节拍计数重置为 0,然后开始播放。 -
然后,我们计算下一个时钟滴答的时间,并让线程休眠至该时间(请参见前面关于滴答时间的解释)。醒来后,我们发送
clock消息,这将在每个节拍时发生。 -
最后,如果调用
stop方法,self._stop_signal将被设置为True,这将退出循环,并发送stop消息。
-
- 让我们初始化线程并启动它:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--midi_port", type=str, default="magenta_out")
args = parser.parse_args()
def send_clock():
output_ports = [name for name in mido.get_output_names()
if args.midi_port in name]
midi_hub = MidiHub(input_midi_ports=[],
output_midi_ports=output_ports,
texture_type=None)
outport = midi_hub._outport
# Starts the metronome at 120 QPM
metronome = Metronome(outport, 120)
metronome.start()
# Waits for 16 seconds and send the stop command
metronome.join(timeout=16)
metronome.stop()
return 0
if __name__ == "__main__":
send_clock()
以下列表将进一步解释:
-
代码与我们之前的示例类似。我们首先改变的内容是,我们保留对
midi_hub._outport端口的引用,以便将 MIDI 时钟发送到该端口。 -
然后,我们使用
outport初始化Metronome类,并通过start启动它。这将执行线程中的run方法。 -
然后,我们通过 16 秒的超时调用
join方法,意味着我们将在退出并调用stop方法之前播放 8 小节。我们这样做只是为了展示stop方法的使用及其对 Ableton Live 的影响。
- 在 Ableton Live 中,我们需要确保 Sync 按钮已为
magenta_out端口 开启:
- 一旦完成这一步,我们需要确保屏幕左上角的 Ext 按钮已激活:
Ext 按钮,代表 External,意味着 Ableton 不会使用其内部时钟,而是依赖外部时钟源。
大多数 DAW 和硬件合成器都有类似的 External 选项,但默认情况下通常是禁用的。确保查看如何为你正在同步的软件或硬件激活该选项。
在 Ext 按钮右侧,两个指示器显示进出 MIDI 时钟消息,这对于调试非常有用。我们还突出显示了以下内容:
-
-
QPM 指标将在播放过程中更新为 120(目前为了测试目的,设置为 110 QPM)
-
Arrangement position 部分,显示 9.1.1,这是当我们的 Python 程序退出并发送
stop消息时节拍计数的值(因为我们在 8 小节后停止) -
Transport section 部分,其中包含开始、停止和录音按钮,当我们启动和停止程序时,按钮会更新
-
现在,我们可以向 Ableton Live 发送 MIDI 时钟。
- 最后,启动我们的 Magenta 应用程序:
> python chapter_09_example_04.py --midi_port="magenta_out"
在 Ableton Live 中,你应该看到 BPM 改为 120 QPM。虽然可能需要一些时间才能达到这个值,并且可能会在稳定过程中上下波动,但最终应该稳定在 120 QPM。16 秒后,Ableton Live 应该停止,最终的节拍计数为 8 个完整的节拍(显示为 9.1.1)。
使用 MIDI 控制消息
发送 MIDI 时钟是最常见的设备同步方式,因为所有设备都支持 MIDI 时钟。另一种将 Magenta 与 DAW 同步的方法是使用 MIDI 控制消息。
MIDI 控制消息是一个发送control和value的消息。例如,我们可以使用以下 Mido 消息来发送 MIDI 控制:mido.Message(type="control_change", control="...", value"...")。让我们定义一些控制消息来执行我们想要的操作:
-
开始/停止:用于启动和停止传输,用于同步相位(分别使用
control="1"和control="2")。 -
QPM:这是在传输开始前设置节奏的方式(使用
control="3")。
这只是一个控制值的示例;你可以使用任何你想要的值,只要它在 DAW 端正确映射即可。在大多数 DAW 中,将控制消息映射到输入是很容易的。通常,DAW 会提供一个learn功能,激活后,它会将选定的输入映射到接下来到来的任何 MIDI 消息。
让我们在 Ableton Live 中尝试一下:
-
使用右上角的MIDI按钮激活 MIDI 映射模式(Ableton 中的所有可映射输入会变成紫色)。
-
选择你想映射的输入(例如QPM),然后发送相应的 MIDI 控制消息(参见前面的代码片段),它将把输入映射到控制消息。
-
在接收到 MIDI 控制消息后,Ableton 中的输入将会与之映射。
-
退出 MIDI 映射模式,然后发送相同的 MIDI 控制消息。映射的输入应该会被激活。
一旦我们所有的输入都被映射,我们就可以从 Magenta 应用程序发送相应的消息,按需开始、停止或更改 QPM。例如,Magenta 应用程序可以在开始之前发送 QPM,然后在发送第一个 MIDI 音符时,同时发送 MIDI 控制消息开始。
这种方法的缺点是,如果两个应用程序中的任何一个出现不同步的情况,就无法在不停止并重新启动播放的情况下将它们重新同步。另一方面,MIDI 时钟则会持续地同步设备。
使用 Ableton Link 同步设备
Ableton Link (github.com/Ableton/lin…)是一个旨在同步软件设备的开源标准。它支持在本地网络上的自动发现,并且易于使用。现在许多 DAW 都支持 Ableton Link,这又是另一种将 Magenta 应用程序与 DAW 同步的方式,但需要实现该规范。
向硬件合成器发送 MIDI
向硬件合成器发送 MIDI 与我们在前面章节中的操作非常相似,唯一不同的是硬件合成器需要自己打开一个新的 MIDI 端口(就像 FluidSynth 一样),所以我们不需要为它创建虚拟端口。
我们将使用 Arturia BeatStep Pro 作为示例,但这应该适用于任何支持 MIDI 的设备:
-
首先,我们需要为合成器安装驱动程序,是否需要安装取决于合成器和平台。
-
然后,我们通过 USB 将合成器连接到计算机,并运行第一个示例,以找出已声明的 MIDI 端口。对于 Windows 上的 Arturia BeatStep Pro,我们有输出端口
MIDIIN2 (Arturia BeatStep Pro) 1。 -
现在,我们可以通过将 Magenta 输出端口更改为合成器输入端口来运行之前的示例:
> python chapter_09_example_03.py --midi_port="MIDIIN2 (Arturia BeatStep Pro) 1"
这应该直接将 MIDI 发送到硬件合成器。
此示例使用 USB MIDI 发送 MIDI,然而并不是所有合成器都支持这种方式。有些合成器仅支持通过 MIDI 电缆连接,而非 USB 电缆,这意味着你需要一个声卡或 USB 转 MIDI 转换器。过程依然相同,但你必须通过声卡或转换器。
将 Magenta 作为独立应用程序与 Magenta Studio 一起使用
Magenta Studio 是最接近 Magenta 独立应用程序的工具,因为它不需要任何安装,也不需要了解任何技术来使其工作。这一点尤为重要,因为 Magenta 及其技术是复杂的,但最终,每个人都能使用它 这一点是非常重要的。
我们将了解 Magenta Studio 的工作原理,并找到我们在前几章中已经覆盖过的许多元素。Magenta Studio 有两种打包方式:
-
如 Ableton Live 插件 (magenta.tensorflow.org/studio/able…),它通过 Max for Live 集成和 Magenta.js 应用程序将 Magenta 集成到 Ableton Live 中(支持 Windows 和 macOS)
-
如 独立应用程序 (magenta.tensorflow.org/studio/stan…),它们是 Electron 应用程序(支持所有平台)
我们不会过多讨论独立应用程序,因为我们已经涵盖了关于它们的所有必要知识。实际上,Electron 应用程序是一个带有其运行时和 Chromium 浏览器的 Node.js 应用程序,因此我们已经在前一章 第八章 中讲解了这些内容,Magenta.js 中的 Magenta 浏览器。
查看 Magenta Studio 的内容
由于这两种打包方式都基于 Magenta.js,它们包含相同的功能:
-
CONTINUE 使用 MusicRNN(基于 LSTM),根据使用情况选择 DrumsRNN 模型或 MelodyRNN 模型,从一个引导器开始继续一个序列。
-
GENERATE 使用 MusicVAE 模型,使用一个 4 小节的模型来生成鼓点或旋律。
-
INTERPOLATE 也使用 MusicVAE 模型。
-
GROOVE 使用 GrooVAE 模型为量化序列添加 groove。
-
DRUMIFY 使用 GrooVAE tap 模型将 tap 序列 转换为 鼓点序列。
下载独立版本时,你将能够安装任何五个应用程序(取决于平台使用 .exe 或 .dmg)。安装并启动后,应用程序将如下所示:
你可以找到我们之前讨论过的许多参数:温度、长度、变化(生成序列的数量)、步数(插值的数量)等。独立应用程序和 Ableton 包装版本的区别在于它们如何与我们的音乐工具集成:独立应用程序可以处理磁盘上的文件(如前述截图所示,使用选择文件...按钮),而 Ableton Live 插件则可以直接读取和写入Session View中的剪辑。
让我们来看看 Ableton Live 插件的集成。
在 Ableton Live 中集成 Magenta Studio
Magenta Studio 在 Ableton Live 中的插件集成非常棒,因为它符合机器学习增强音乐制作环境的理念。一般来说,Magenta 在现有工具中的集成非常重要,Magenta Studio 就是一个很好的例子。
了解 Ableton Live 插件的设计很有趣,因为它非常巧妙。在 Ableton Live 中,你可以将 Max MSP 应用程序作为插件或设备集成。Max MSP(cycling74.com/products/ma…)是一个强大的音乐视觉编程语言。Ableton Live 插件的工作方式如下:
-
Ableton Live 启动了
magenta.amxd补丁,这是一个 Max MSP 程序。 -
Max MSP 程序会在 Ableton Live 中显示一个 UI 界面,我们可以选择Continue、Generate等程序。
-
选择后,Max MSP 程序将启动一个 Node.js 进程,包含 Magenta.js 应用程序(与独立应用程序相同)。
-
使用 Max MSP API,Magenta.js 应用程序可以查看 Ableton Live 的Session View内容,包括剪辑和轨道,并进行内容写入。
目前,Magenta Studio 仅在 Ableton Live 中集成。未来可能会集成其他 DAW,因为 Magenta Studio 的实现并没有什么特定于 Ableton Live 的内容。
为了使这个示例生效,我们需要 Ableton Live 10.1 Suite 版,因为 Magenta Studio 的运行需要集成 Max For Live(仅在Suite版中可用)。如果你没有该程序,可以在www.ableton.com/en/trial/尝试演示版。
让我们通过一个完整的示例来演示Continue应用程序:
-
从magenta.tensorflow.org/studio/able…下载适用于你平台的 Max MSP 补丁,点击下载按钮,这将下载
magenta_studio-VERSION-windows.amxd文件。 -
打开 Ableton Live,创建一个新的 MIDI 轨道,将文件拖放到 MIDI 轨道设备中(加载可能需要一些时间):
在前面的截图中,我们看到我们从之前的示例中录制了两个 MIDI 片段,MIDI from Magenta 1 和 MIDI from Magenta 2,我们将使用这些片段通过 Continue 插件生成新内容。我们可以在 Magenta Studio Plugin 轨道的底部看到 Magenta Studio 补丁。
- 现在,让我们点击 Magenta Studio 插件中的 CONTINUE。你应该看到 Continue Node.js 应用程序启动:
在 Input Clip 部分,我们从 MIDI from Magenta 轨道中添加了 MIDI from Magenta 2 的 MIDI 片段,这将由 DrumsRNN 模型作为启动器使用。四种变化将在启动器片段后自动添加到 Ableton Live 中,名称为 x/4 [MIDI from Magenta 2],其中 x 是生成的片段的索引。
总结
在本章中,我们讨论了 Magenta 与已建立的音乐制作软件的互动。
首先,我们展示了如何将 MIDI 从 Magenta 发送到 DAW 或合成器。我们首先使用 Mido,这是一个强大的 Python 库,用于处理 MIDI 操作,查看了 MIDI 端口。我们展示了如何在 Magenta 中循环 MIDI 的示例,这需要正确的时序和线程工具。我们还讨论了 Magenta 和 DAW 之间的同步,使用了各种方法,最著名的是使用 MIDI 时钟消息和传输消息。我们通过展示 Magenta 如何直接将 MIDI 发送到硬件合成器(如键盘)来结束 MIDI 部分。
最后,我们介绍了 Magenta Studio,既作为独立应用程序,又作为 Ableton Live 插件。我们查看了它在 Ableton Live 中的整合,以及将 Magenta 集成到现有音乐工具中的重要性。
观察 Magenta 在音乐制作生态系统中的整合是完美的结尾。它提醒我们,Magenta 不是一个独立的终点,而是一个需要与其他音乐制作工具结合使用才能真正有用的工具。通过开发像 Magenta.js 和 Magenta Studio 这样的项目,Magenta 正在变得更加易于更广泛的非技术用户使用。
在提高 Magenta 对所有人可用性方面,仍然有很多工作要做。然而,这是一个伟大音乐制作工具的开始。
问题
-
软件合成器(如 FluidSynth)和数字音频工作站(DAW)(如 Ableton Live)之间有什么区别?
-
为什么打开 MIDI 虚拟端口是使音乐软件相互互动所必需的?
-
基于
chapter_09_example_03.py编写代码,而不是循环四小节的序列,每四小节生成一个新序列。 -
为什么基于 MIDI 控制消息的同步不够稳定?
-
为什么 Magenta Studio 在音乐创作生态系统中如此重要?
-
Magenta Studio 插件和 Magenta Studio 独立版背后的技术是什么?
进一步阅读
-
Learn Live (Ableton Live):关于 Ableton Live 的精彩教程,是目前关于音乐制作的最佳教程,涵盖了多个高级内容,适用于许多 DAW(www.ableton.com/en/live/lea…)
-
会话视图 (Ableton Live):关于 Ableton Live 会话视图的更多信息,适用于使用 Magenta Studio(www.ableton.com/en/manual/s…)
-
社区学习 (Bitwig):Bitwig 的优秀教程(www.bitwig.com/en/communit…)
-
教程 (Reason):Reason 的教程以博客文章的形式提供(www.reasonstudios.com/blog/catego…)
-
开始使用 SC (SuperCollider):进入 SuperCollider 及其编程语言
scalang的最佳方式——示例也与软件一起打包下载(doc.sccode.org/Tutorials/G…) -
VCV Rack 手册 (VCV Rack):VCV 文档及开发者 API,供你编写软件代码使用(vcvrack.com/manual/)
-
端口:关于虚拟 MIDI 端口在不同平台间差异的 Mido 文档(mido.readthedocs.io/en/latest/p…)
-
MIDI 消息总结:MIDI 消息列表,包括我们使用的 MIDI 时钟和传输消息(www.midi.org/specificati…)
-
消息类型:Mido 支持的消息类型,来自 MIDI 规范(mido.readthedocs.io/en/latest/m…)
-
Magenta Studio:Magenta 团队关于 Magenta Studio 的博客文章(magenta.tensorflow.org/studio-anno…)
第十二章:评估
第一章:Magenta 与生成艺术简介
-
随机性。
-
马尔可夫链。
-
Algorave。
-
长短期记忆(LSTM)。
-
自主系统生成音乐,无需操作员输入;辅助音乐系统将在创作时补充艺术家的工作。
-
符号化:乐谱、MIDI、MusicXML、AbcNotation。子符号化:原始音频(波形)、频谱图。
-
"音符开启"与"音符关闭"的时序、音高范围为 1 到 127 kHz、速度和通道。
-
在 96 kHz 的采样率下,奈奎斯特频率为 96 kHz/2 = 48 kHz,频率范围为 0 到 48 kHz。这对于听音频来说效果较差,因为 28 kHz 的音频在耳朵上是听不见的(记住,超过 20 kHz 的声音是无法听到的),而且这种采样率并不被很多音频设备正确支持。不过,在录音和音频编辑中它还是有用的。
-
单个音乐音符 A4 被响亮地演奏 1 秒钟。
-
鼓、声部(旋律)、和声(复音)、插值和操作。
第二章:使用鼓 RNN 生成鼓序列
-
给定一个当前序列,预测下一个音符的乐谱,然后对每一个你希望生成的步骤进行预测。
-
(1) RNN(递归神经网络)对向量序列进行操作,用于输入和输出,这对于像乐谱这样的序列数据非常适用;(2) 保持一个由前一输出步骤组成的内部状态,这对于基于过去输入做出预测非常有用,而不仅仅是基于当前输入。
-
(1) 首先,隐藏层将得到 h(t + 1),即前一个隐藏层的输出;(2) 它还将接收 x(t + 2),即当前步骤的输入。
-
生成的音符小节数将是 2 小节,或者 32 个步骤,因为每小节有 16 个步骤。在 80 QPM 下,每个步骤的时长为 0.1875 秒,因为你将一分钟的秒数除以 QPM,再除以每小节的步骤数:60 / 80 / 4 = 0.1875。最后,你有 32 个步骤,每个步骤时长 0.1875 秒,所以总时间为 32 * 0.1875 = 6 秒。
-
增加分支因子会减少随机性,因为你有更多的分支可以选择最佳分支,但增加温度会增加随机性。两者同时进行会互相抵消,只是我们不知道这种抵消的比例。
-
在每个步骤中,算法将生成四个分支并保留两个。在最后一次迭代中,束搜索将通过检查每一层剩余的两个节点与生成图的步骤数(即树的高度)相乘来搜索图中最佳的分支,这个数是三。因此,我们会遍历 2 * 3 个节点 = 6 个节点。
-
NoteSequence。 -
MIDI 音符映射到以下类别:36 映射到 0(踢鼓),40 映射到 1(军鼓),42 映射到 2(闭合高帽)。计算得到的索引为 2⁰ + 2¹ + 2² = 7,因此得到的向量为 v = [0, 0, 0, 0, 0, 0, 1, 0, ... ]。
-
索引 131 的位表示是
10000011(在 Python 中,你可以使用"{0:b}".format(131)来获取)。这是 2⁰ + 2¹ + 2⁷,得到以下类别:0(鼓组)、1(小军鼓)和 7(撞击钹)。然后,我们任意选择每个类别的第一个元素: {36, 38, 49}。
第三章:生成复调旋律
-
消失梯度(在每个 RNN 步骤中,值被小值相乘)和梯度爆炸是常见的 RNN 问题,通常发生在反向传播步骤的训练过程中。LSTM 提供了一个专用的细胞状态,通过遗忘门、输入门和输出门进行修改,以缓解这些问题。
-
门控 递归 单元(GRUs)是更简单但表达力较弱的记忆单元,其中遗忘门和输入门合并为一个单一的更新门。
-
对于 3/4 拍的节奏标记,你需要每四分之一音符 3 步,每四分之一音符 4 步,总共每小节 12 步。对于一个二进制步数计数器,要计数到 12,你需要 5 个比特位(像 4/4 拍那样),它们只能计数到 12。对于 3 个回顾,你需要查看过去 3 小节,每小节 12 步,所以你得到 [36, 24, 12]。
-
结果向量是前一步向量的和,每个向量都应用了注意力掩码,因此我们对 [1, 0, 0, 0] 应用了 0.1,对 [0, 1, 0, x] 应用了 0.5,得到了 [0.10, 0.50, 0.00, 0.25]。x 的值是 0.5,因为 0.5 乘以 0.5 等于 0.25。
-
一个 C 大调和弦,时值为一个四分音符。
-
在 Polyphony RNN 中,没有音符结束事件。如果在一个步骤中没有使用
CONTINUED_NOTE来表示某个音高,则该音符会停止。在 Performance RNN 中,则会使用NOTE_END 56事件。 -
(1) 使用
TIME_SHIFT事件表达的时序性,这些事件在所有的 Performance RNN 模型中都有,例如在performance配置中,以及(2) 使用VELOCITY事件的动态播放,这些事件出现在performance_with_dynamics配置中。 -
它将在 RNN 步骤调用期间改变迭代次数。每秒钟的音符数量越大,在生成过程中所需的 RNN 步骤就越多。
第四章:使用 MusicVAE 进行潜在空间插值
-
主要用途是降维,迫使网络学习重要特征,从而使得重构原始输入成为可能。AE 的缺点是隐藏层表示的潜在空间不是连续的,难以进行采样,因为解码器无法理解某些点。
-
重建损失在网络生成与输入不同的输出时进行惩罚。
-
在 VAE 中,潜在空间是连续和平滑的,使得可以对空间中的任何点进行采样,并在两点之间进行插值。它是通过让潜在变量遵循 P(z) 的概率分布来实现的,通常是高斯分布。
-
KL 散度衡量两个概率分布之间的差异。当与重构损失结合时,它会将聚类集中在 0 附近,并使它们更接近或更远离彼此。
-
我们使用
np.random.randn(4, 512)来采样正态分布。 -
计算潜在空间中两点之间的方向。
第五章:使用 NSynth 和 GANSynth 进行音频生成
-
你需要处理每秒 16,000 个样本(至少),并在更大的时间尺度上跟踪整体结构。
-
NSynth 是一个 WaveNet 风格的自编码器,能够学习自己的时间嵌入,使得捕捉长期结构成为可能,并提供访问有用的隐藏空间。
-
彩虹图中的颜色代表了时间嵌入的 16 个维度。
-
请查看章节代码中
audio_utils.py文件中的timestretch方法。 -
GANSynth 使用上采样卷积,使得整个音频样本的训练和生成处理可以并行进行。
-
你需要使用
np.random.normal(size=[10, 256])来采样随机正态分布,其中 10 是采样的乐器数量,256 是潜在向量的大小(由latent_vector_size配置给出)。
第六章:训练数据准备
-
MIDI 不是文本格式,因此更难使用和修改,但它非常常见。MusicXML 相对较少见且笨重,但它的优势在于采用文本格式。ABCNotation 也相对罕见,但其优势在于是文本格式,并且更接近乐谱。
-
使用
chapter_06_example_08.py中的代码,并在提取过程中更改program=43。 -
LMD 中有 1,116 首摇滚歌曲,3,138 首爵士、蓝调和乡村歌曲。请参考
chapter_06_example_02.py和chapter_06_example_03.py,了解如何根据音乐风格信息进行统计。 -
在
melody_rnn_pipeline_example.py中使用RepeatSequence类。 -
使用
chapter_06_example_09.py中的代码。是的,我们可以用它训练一个量化模型,因为数据准备管道会量化输入。 -
对于小型数据集,数据增强在创造更多数据方面起着至关重要的作用,因为有时你根本没有更多数据。对于较大的数据集,数据增强也起到作用,通过创建更多相关数据和现有数据的变种,帮助网络的训练阶段。
第七章:训练 Magenta 模型
-
请参见
chapter_07_example_03.py。 -
欠拟合的网络是指尚未达到最佳状态的网络,这意味着它在评估数据上无法做出良好预测,因为它对训练数据的拟合不佳(目前为止)。可以通过让其训练足够长时间、增加网络容量和更多数据来修正。
-
过拟合的网络是指已学会预测输入数据,但无法推广到训练集之外的值的网络。可以通过增加更多数据、减少网络容量,或使用正则化技术(如 dropout)来修正。
-
提前停止。
-
阅读《关于深度学习大批量训练的问题:泛化差距与锐化极小值》,其中解释了更大的批量大小会导致锐化极小值,从而导致泛化能力较差。因此在效率方面更差,但在训练时间方面可能更好,因为可以同时处理更多数据。
-
更大的网络会在预测中更加精确,因此最大化这一点很重要。网络的规模也应随数据的大小(和质量)增长。例如,对于数据而言过大的网络可能会导致过拟合。
-
它有助于解决梯度爆炸问题,因为权重将乘以较小的值,限制大梯度的可能性。另一种方法是降低学习率。
-
可用于在更强大的机器上启动训练,也可以同时启动多个训练会话。不幸的是,使用云提供商会产生成本,这意味着我们使用的训练时间和功率越多,成本就会越高。
第八章:在浏览器中使用 Magenta.js
-
我们可以使用 TensorFlow.js 训练模型,但无法使用 Magenta.js 训练模型。我们需要使用 Python 在 Magenta 中训练模型,然后在 Magenta.js 中导入生成的模型。
-
Web Audio API 允许在浏览器中使用音频节点进行音频合成、转换和路由。最简单的使用方法是使用像 Tone.js 这样的音频框架。
-
方法是
randomSample,参数是生成音符的音高。例如,使用 60 将生成 MIDI 音高为 60 的单音符,或者在字母符号中是 C4。这也是使用 Tone.js 调高或调低音符音高的参考。 -
方法是
sample,乐器数量取决于正在使用的模型。在我们的示例中,我们使用了trio模型,它生成三种乐器。使用melody模型将只生成一个主导乐器。 -
因为 JavaScript 是单线程的,如果在 UI 线程中启动长时间的同步计算,将会阻塞其执行。使用 Web Worker 可以在另一个线程中执行代码。
-
在浏览器中使用 Web MIDI API,目前支持不够完善,或者在服务器端使用 Magenta.js 进程,这样可以更容易地向其他进程发送 MIDI。
第九章:使 Magenta 与音乐应用程序互动
-
DAW 将具有更多面向音乐制作的功能,如录制、音频、MIDI 编辑、效果和母带处理以及歌曲作曲。像 FluidSynth 这样的软件合成器功能较少,但优点是轻量且易于使用。
-
大多数音乐软件不会自动打开 MIDI 端口,因此要在它们之间发送序列,我们必须手动打开端口。
-
参见本章的代码
chapter_09_example_05.py。 -
因为将两个失去同步的软件重新同步需要重启它们。MIDI 时钟可以在每个节拍时启用同步。
-
因为 Magenta Studio 与现有的音乐制作工具(如 DAW)集成,并且不需要任何技术知识,它使得 AI 生成的音乐能够面向更广泛的受众,这也是 Magenta 的最终目标。
-
Magenta Studio 插件和 Magenta Studio 独立版都基于 Magenta.js,并通过 Electron 打包。Magenta Studio 插件使用 Ableton Live 中的 Max MSP 集成来执行。