numpy 有没有一种方法可以在Python中创建一个简单的用户交互式音乐合成器

rkkpypqq  于 2023-05-07  发布在  Python
关注(0)|答案(1)|浏览(86)

我目前正在尝试使用pygame.mixersounddevice编写一个python合成器,以输出我在numpy array中创建的正弦波样本。在创建正弦波之前,必须说明波形的持续时间,例如:sin(frequency * 2 * Pi * duration)因此,你如何在用户按键的持续时间内播放此声音。
当我阅读时,没有太多关于python的文章看起来很容易理解,因此任何帮助都将受到赞赏。
另外,如果有人能解释或给予一个使用python缓冲区对象的sounddevice.Streamsounddevice.RawStream如何工作的例子,如果它对我的情况有帮助,那将非常感谢。
我已经尝试过使用sounddevice.play(),但这似乎是我试图实现的基本功能。我也试过创建一小段正弦波,并循环它的用户输入,但这不会工作,当我来调制的声音。
我不喜欢使用sounddevice.play()的另一个原因是因为你需要延迟程序,因为我已经使用了sounddevice.wait(),如果不是程序运行到最后没有播放任何东西。
在观看这段视频时…https://www.youtube.com/watch?v=tgamhuQnOkM ...它使用c++来编程一个synth,他使用了一个单独的模块,我认为它运行一个后台线程,但他的模块单独获取每个样本,而不是作为一个数组。
我也试过使用pygame.sndarray.make_sound()。这是一个例子,当/如果synth工作时,我想做的事情:

import numpy as np # download
            import sounddevice as sd # download
            import time

            stream = []

            # Main Controls
            sps = 44100 # DON'T CHANGE

            carrier_hz = 440.0

            duration_s = 1.0

            atten = 0.3

            def amplitudeMod(t_samples, carrier):
                # Modulate the amplitude of the carrier
                ac = 0.2 # amplitude 0 = min, 1 = max
                ka = 1.0 # range of change 0.1 = less, 1.0 = most
                modulator_hz = 0.0 # frequency of modulation 20hz max
                modulator = np.sin(2 * np.pi * modulator_hz * t_samples / sps)
                envelope = ac * (1.0 + ka * modulator)
                return carrier * envelope

            def frequencyMod(t_samples, sps):
                # Modulate the frequency of the carrier
                k = 50.0 # range of change 0.1 = less, ?? = most
                modulator_hz = 10.0 # frequency of modulation
                carrier2 = 2 * np.pi * t_samples * carrier_hz / sps
                modulator = k * np.sin(2 * np.pi * t_samples * modulator_hz / sps)
                return np.cos(carrier2 + modulator)

            # Create carrier wave
            t_samples = np.arange(duration_s * sps)
            carrier = np.sin(2 * np.pi * carrier_hz * t_samples / sps)

            choice = input("1) Play sine\n2) Play amplitude modulation\n3) Play frequency modulation\n;")
            if choice == "1":
                output = carrier
            if choice == "2":
                output = amplitudeMod(t_samples, carrier)
            if choice == "3":
                output = frequencyMod(t_samples, sps)

            # Output

            output *= atten

            sd.play(output, sps)
            sd.wait()
            sd.stop()

有没有办法把它创建成一个pygame按键事件,它只在按键被按下时播放正弦波,然后在按键被释放时停止。

ttisahbt

ttisahbt1#

在尝试了几个库(包括pygame,它似乎不容易支持动态生成和更改音频流)之后,我能够使用Python sounddevice库制作一个可控的音调生成器。我正在运行MacOS Monterey(Intel)和Python 3.11。

  • 下面的示例包括一个最小的tkinter GUI,它将按键和释放事件发送到ToneGenerator
  • ToneGenerator类启动sounddevice.OutputStream(),它在程序执行期间运行
  • 按键调用ToneGenerator.note_event()将事件传递给ToneGenerator。这些改变了频率,并开始一个简单的攻击-释放包络的斜升或斜降。包络斜坡时间在_get_scaler_envelope函数中设置。
  • 这表明您可以在sounddevice流式传输音频时修改流(频率和振幅)。
  • 为了使示例简短(并且因为我仍在研究进一步的功能),我在注解中做了许多简化的假设。
  • 最后,我尝试并发运行多个ToneGenerator对象,以便同时演奏多个音符(尽管没有使用此键盘接口)。这种实现看起来至少提供了某种程度的复调,但是将键路由到音调生成器的问题留给了进一步的开发。我还没有彻底测试复调或做任何基准测试。
  • 下面是一个很长但很小的代码示例(我希望-我是这个网站的新手,可能需要几次才能正确地发布代码)!
  • 请确保您使用计算机的正确设备号调用ToneGenerator(程序会将设备列表打印到您的控制台)。
""" Minimal example music synthesis using sounddevice library
    Adapted from:  https://python-sounddevice.readthedocs.io/en/0.4.6/examples.html#play-a-sine-signal
    plays notes when key pressed and stops at key release with fixed velocity (set in self.amplitude)
    sine and saw are provided
    simple attack-release envelope
    monophonic - only plays one key at a time
    it appears that multiple Tone classes can be run in parallel for polyphony if a key router is added
    Simplifications to keep this example short:
        monophonic and does not allow changing note while note is playing (it would cause popping)
        envelop attack and release take at least a whole sample (11.6ms) rather than starting mid-sample
        no modulators (other than envelope) and no filters
        GUI is only used for non-blocking keyboard input - all other parameters are coded
        GUI interprets auto-repeating keys (when held) as separate press and release events (no debouncing)
    Intended to answer:
    https://stackoverflow.com/questions/54641717/is-there-a-way-to-create-a-simple-user-interactive-music-synth-in-python
"""

import sounddevice as sd  # https://python-sounddevice.readthedocs.io/en/0.4.6/
import numpy as np
import time
import tkinter as tk

class GUI(tk.Tk):
    def __init__(self, note_command):
        super().__init__()
        self.note_command = note_command  # called upon key press and release
        self.keys = {'c': 60, 'd': 62, 'e': 64, 'f': 65, 'g': 67, 'a': 69, 'b': 71, 'o': 72}  # midi notes (o is high c)
        tk.Label(self, text='press and release a key to play a note:\nc, d, e, f, g, a, b, or o for high c').\
            pack(padx=10, pady=10)
        self.message_label = tk.Label(self, font=('Helvetica', 36), width=20)
        self.message_label.pack(padx=20, pady=20)

        for key in self.keys:
            self.bind(f'<{key}>', self.key_press_event)
            self.bind(f'<KeyRelease-{key}>', self.key_release_event)

    def key_press_event(self, event):
        self.message_label.config(text=f'Key {event.char} (MIDI {self.keys[event.char]}) pressed')
        self.note_command(self.keys[event.char], True)

    def key_release_event(self, event):
        self.message_label.config(text=f'Key {event.char} (MIDI {self.keys[event.char]}) released')
        self.note_command(self.keys[event.char], False)

class ToneGenerator:
    @staticmethod
    def list_devices(): print(sd.query_devices())  # call to get list of available audio devices

    @staticmethod
    def note_to_f(midi_note: int): return 440.0 * 2 ** ((midi_note - 69) / 12)

    def __init__(self, device: int, waveform: str = 'sine'):
        self.device = device  # sd device
        self.waveform = waveform
        self.frequency = 440.0  # frequency in Hz of note (can't be zero, so set to any value before first note)
        self.amplitude = 0.0  # 0.0 <= amplitude <= 1.0  amplitude of note
        self.stream = None  # sd.OutputStream object
        self.stream_start_time = None
        self.prev_callback_time = None
        self.note_on_time = None  # for envelope
        self.note_off_time = None  # for envelope
        self.start_idx = 0  # index for callbacks
        self.samplerate = 44100

        def callback(outdata, frames, time, status):  # callback from sd.OutputStream
            if self.prev_callback_time is None:
                self.prev_callback_time = self.stream.time
            elapsed = self.stream.time - self.prev_callback_time
            self.prev_callback_time += elapsed
            np_env: np.ndarray = self.get_envelope(frames, elapsed)
            t = (self.start_idx + np.arange(frames)) / self.samplerate
            t = t.reshape(-1, 1)

            if self.waveform == 'sine':
                outdata[:] = np_env * np.sin(2 * np.pi * self.frequency * t)
            elif self.waveform == 'saw':
                outdata[:] = np_env * 2 * (t % (1/self.frequency) * self.frequency - .5)
            else:
                raise ValueError(f'ToneGeneraotor: invalid waveform {self.waveform}')
            self.start_idx += frames

        self.stream = sd.OutputStream(device=device, channels=1, callback=callback)
        self.stream.start()  # returns immediately, stream continues  until killed

    def note_event(self, note: int, press: bool = True):
        """ note is midi note number, press indicates whether key pressed or released
        """
        if press:
            self.note_on_time = time.time()
            self.note_off_time = None
            self.frequency = ToneGenerator.note_to_f(note)
            self.amplitude = 1.0  # computer keys aren't velocity sensitive!
        else:
            self.note_on_time = None
            self.note_off_time = time.time()

    def get_envelope(self, frames: int, sample_time: float) -> np.ndarray:
        current = self._get_scaler_envelope()
        previous = self._get_scaler_envelope(time_delta = -sample_time)
        return np.linspace(previous, current, num=frames).reshape((frames, 1))

    def _get_scaler_envelope(self, time_delta: float = 0.0) -> float:  # helper for get_envelope
        attack_time = .01  # seconds
        release_time = .4  # seconds
        env = 0.0
        if self.note_on_time is not None:
            env = max(0, min((1.0, (time.time() + time_delta - self.note_on_time) / attack_time)))
        elif self.note_off_time is not None:
            env = min(1, max((0, 1 - (time.time() + time_delta - self.note_off_time) / release_time)))
        return env * self.amplitude

if __name__ == '__main__':
    ToneGenerator.list_devices()
    sd = ToneGenerator(device=5, waveform='saw')  # set device based on output from ToneGenerator.list_devices()
    app = GUI(sd.note_event)
    app.mainloop()

相关问题