Пример #1
0
class SynthGUI(tk.Frame):
    def __init__(self, master=None):
        super().__init__(master)
        self.master.title("Synthesizer")
        self.keypress_sema = Semaphore(1)
        self.osc_frame = tk.Frame(self)
        self.oscillators = []
        self.piano_frame = tk.Frame(self)
        self.piano = PianoKeyboardGUI(self.piano_frame, self)
        self.piano.pack(side=tk.BOTTOM)
        filter_frame = tk.LabelFrame(self, text="Filters etc.", padx=10, pady=10)
        self.envelope_filters = [
            EnvelopeFilterGUI(filter_frame, "1", self),
            EnvelopeFilterGUI(filter_frame, "2", self),
            EnvelopeFilterGUI(filter_frame, "3", self)]
        self.echo_filter = EchoFilterGUI(filter_frame, self)
        for ev in self.envelope_filters:
            ev.pack(side=tk.LEFT, anchor=tk.N)
        self.arp_filter = ArpeggioFilterGUI(filter_frame, self)
        self.arp_filter.pack(side=tk.LEFT, anchor=tk.N)
        f = tk.Frame(filter_frame)
        self.tremolo_filter = TremoloFilterGUI(f, self)
        self.tremolo_filter.pack(side=tk.TOP)
        lf = tk.LabelFrame(f, text="A4 tuning")
        lf.pack(pady=(4, 0))
        lf = tk.LabelFrame(f, text="Performance")
        self.samplerate_choice = tk.IntVar()
        self.samplerate_choice.set(44100)
        tk.Label(lf, text="Samplerate:").pack(anchor=tk.W)
        subf = tk.Frame(lf)
        tk.Radiobutton(subf, variable=self.samplerate_choice, value=44100, text="44.1 kHz", pady=0, command=self.create_synth).pack(side=tk.LEFT)
        tk.Radiobutton(subf, variable=self.samplerate_choice, value=22050, text="22 kHz", pady=0, command=self.create_synth).pack(side=tk.LEFT)
        subf.pack()
        tk.Label(lf, text="Piano key response:").pack(anchor=tk.W)
        subf = tk.Frame(lf)
        self.rendering_choice = tk.StringVar()
        self.rendering_choice.set("realtime")
        tk.Radiobutton(subf, variable=self.rendering_choice, value="realtime", text="realtime", pady=0).pack(side=tk.LEFT)
        tk.Radiobutton(subf, variable=self.rendering_choice, value="render", text="render", pady=0).pack(side=tk.LEFT)
        subf.pack()
        lf.pack(pady=(4, 0))
        f.pack(side=tk.LEFT, anchor=tk.N)
        self.echo_filter.pack(side=tk.LEFT, anchor=tk.N)
        misc_frame = tk.Frame(filter_frame, padx=10)
        tk.Label(misc_frame, text="To Speaker:").pack(pady=(5, 0))
        self.to_speaker_lb = tk.Listbox(misc_frame, width=8, height=5, selectmode=tk.MULTIPLE, exportselection=0)
        self.to_speaker_lb.pack()
        lf = tk.LabelFrame(misc_frame, text="A4 tuning")
        self.a4_choice = tk.IntVar()
        self.a4_choice.set(440)
        tk.Radiobutton(lf, variable=self.a4_choice, value=440, text="440 Hz", pady=0).pack()
        tk.Radiobutton(lf, variable=self.a4_choice, value=432, text="432 Hz", pady=0).pack()
        lf.pack(pady=(4, 0))
        tk.Button(misc_frame, text="Load preset", command=self.load_preset).pack()
        tk.Button(misc_frame, text="Save preset", command=self.save_preset).pack()
        for _ in range(5):
            self.add_osc_to_gui()
        self.to_speaker_lb.select_set(4)
        self.osc_frame.pack(side=tk.TOP, padx=10)
        filter_frame.pack(side=tk.TOP)
        misc_frame.pack(side=tk.RIGHT, anchor=tk.N)
        self.piano_frame.pack(side=tk.TOP, padx=10, pady=10)
        self.statusbar = tk.Label(self, text="<status>", relief=tk.RIDGE)
        self.statusbar.pack(side=tk.BOTTOM, fill=tk.X)
        self.pack()
        self.synth = self.output = None
        self.create_synth()
        self.playing_note = False
        self.current_note = None
        self.echos_ending_time = 0
        self.arpeggio_playing = False

    def create_synth(self):
        samplerate = self.samplerate_choice.get()
        self.synth = WaveSynth(samplewidth=2, samplerate=samplerate)
        if self.output is not None:
            self.output.close()
        self.output = Output(self.synth.samplerate, self.synth.samplewidth, 1, queuesize=2)

    def add_osc_to_gui(self):
        osc_nr = len(self.oscillators)
        fm_sources = ["osc "+str(n+1) for n in range(osc_nr)]
        osc_pane = OscillatorGUI(self.osc_frame, self, "Oscillator "+str(osc_nr+1), fm_sources=fm_sources, pwm_sources=fm_sources)
        osc_pane.pack(side=tk.LEFT, anchor=tk.N, padx=10, pady=10)
        self.oscillators.append(osc_pane)
        self.to_speaker_lb.insert(tk.END, "osc "+str(osc_nr+1))

    def create_osc(self, from_gui, all_oscillators, is_audio=False):
        def create_unfiltered_osc():
            def create_chord_osc(clazz, **arguments):
                if is_audio and self.arp_filter.input_mode.get().startswith("chords"):
                    chord_keys = major_chord_keys(self.current_note[0], self.current_note[1])
                    if self.arp_filter.input_mode.get() == "chords3":
                        chord_keys = list(chord_keys)[:-1]
                    a4freq = self.a4_choice.get()
                    chord_freqs = [note_freq(note, octave, a4freq) for note, octave in chord_keys]
                    self.statusbar["text"] = "major chord: "+" ".join(note for note, octave in chord_keys)
                    oscillators = []
                    arguments["amplitude"] /= len(chord_freqs)
                    for f in chord_freqs:
                        arguments["frequency"] = f
                        oscillators.append(clazz(**arguments))
                    return MixingFilter(*oscillators)
                else:
                    # no chord (or an LFO instead of audio output oscillator), return one osc for only the given frequency
                    return clazz(**arguments)
            waveform = from_gui.input_waveformtype.get()
            amp = from_gui.input_amp.get()
            bias = from_gui.input_bias.get()
            if waveform == "noise":
                return WhiteNoise(amplitude=amp, bias=bias, samplerate=self.synth.samplerate)
            elif waveform == "linear":
                startlevel = from_gui.input_lin_start.get()
                increment = from_gui.input_lin_increment.get()
                minvalue = from_gui.input_lin_min.get()
                maxvalue = from_gui.input_lin_max.get()
                return Linear(startlevel, increment, minvalue, maxvalue)
            else:
                freq = from_gui.input_freq.get()
                phase = from_gui.input_phase.get()
                pw = from_gui.input_pw.get()
                fm_choice = from_gui.input_fm.get()
                pwm_choice = from_gui.input_pwm.get()
                if fm_choice in (None, "", "<none>"):
                    fm = None
                elif fm_choice.startswith("osc"):
                    osc_num = int(fm_choice.split()[1])
                    fm = self.create_osc(all_oscillators[osc_num-1], all_oscillators)
                else:
                    raise ValueError("invalid fm choice")
                if pwm_choice in (None, "", "<none>"):
                    pwm = None
                elif pwm_choice.startswith("osc"):
                    osc_num = int(pwm_choice.split()[1])
                    pwm = self.create_osc(all_oscillators[osc_num-1], all_oscillators)
                else:
                    raise ValueError("invalid fm choice")
                if waveform == "pulse":
                    return create_chord_osc(Pulse, frequency=freq, amplitude=amp, phase=phase, bias=bias, pulsewidth=pw, fm_lfo=fm, pwm_lfo=pwm, samplerate=self.synth.samplerate)
                elif waveform == "harmonics":
                    harmonics = self.parse_harmonics(from_gui.harmonics_text.get(1.0, tk.END))
                    return create_chord_osc(Harmonics, frequency=freq, harmonics=harmonics, amplitude=amp, phase=phase, bias=bias, fm_lfo=fm, samplerate=self.synth.samplerate)
                else:
                    o = {
                        "sine": Sine,
                        "triangle": Triangle,
                        "sawtooth": Sawtooth,
                        "sawtooth_h": SawtoothH,
                        "square": Square,
                        "square_h": SquareH,
                        }[waveform]
                    return create_chord_osc(o, frequency=freq, amplitude=amp, phase=phase, bias=bias, fm_lfo=fm, samplerate=self.synth.samplerate)

        def envelope(osc, envelope_gui):
            adsr_src = envelope_gui.input_source.get()
            if adsr_src not in (None, "", "<none>"):
                osc_num = int(adsr_src.split()[1])
                if from_gui is self.oscillators[osc_num-1]:
                    return envelope_gui.filter(osc)
            return osc

        osc = create_unfiltered_osc()
        for ev in self.envelope_filters:
            osc = envelope(osc, ev)
        return osc

    def parse_harmonics(self, harmonics):
        parsed = []
        for harmonic in harmonics.split():
            num, frac = harmonic.split(",")
            num = int(num)
            if '/' in frac:
                numerator, denominator = frac.split("/")
            else:
                numerator, denominator = frac, 1
            frac = float(numerator)/float(denominator)
            parsed.append((num, frac))
        return parsed

    def do_play(self, osc):
        if osc.input_waveformtype.get() == "linear":
            self.statusbar["text"] = "cannot output linear osc to speakers"
            return
        duration = 1
        osc.set_title_status("TO SPEAKER")
        osc.after(duration*1000, lambda: osc.set_title_status(None))
        o = self.create_osc(osc, all_oscillators=self.oscillators, is_audio=True)
        o = self.apply_filters(o)
        sample = self.generate_sample(iter(o), 1)
        with Output(self.synth.samplerate, self.synth.samplewidth, duration) as out:
            out.play_sample(sample, async=True)

    def do_plot(self, osc):
        o = self.create_osc(osc, all_oscillators=self.oscillators)
        o = iter(o)
        frames = [next(o) for _ in range(self.synth.samplerate)]
        if not plot:
            self.statusbar["text"] = "Cannot plot! To plot things, you need to have matplotlib installed!"
            return
        plot.figure(figsize=(16, 4))
        plot.title("Waveform")
        plot.plot(frames)
        plot.show()
        # @todo properly integrate matplotlib in the tkinter gui because the above causes gui freeze problems
        # see http://matplotlib.org/examples/user_interfaces/embedding_in_tk2.html

    def generate_sample(self, oscillator, duration, use_fade=False):
        o = oscillator  # iter(oscillator)
        scale = 2**(8*self.synth.samplewidth-1)
        try:
            frames = [int(next(o)*scale) for _ in range(int(self.synth.samplerate*duration))]
        except StopIteration:
            return None
        else:
            sample = Sample.from_array(frames, self.synth.samplerate, 1)
            if use_fade:
                sample.fadein(0.05).fadeout(0.1)
            return sample

    def continue_play_note(self, oscillator, first=True):
        if self.echos_ending_time and time.time() >= self.echos_ending_time:
            self.stop_playing_note()
            return
        if not self.playing_note:
            sample = self.generate_sample(oscillator, 0.1).fadeout(0.1)
            if sample:
                if sample.samplewidth != self.synth.samplewidth:
                    print("16 bit overflow!")  # XXX
                    sample.make_16bit()
                self.output.play_sample(sample, async=True)
            return
        if first:
            sample = self.generate_sample(oscillator, 0.1)
            if sample:
                sample.fadein(0.05)
                if sample.samplewidth != self.synth.samplewidth:
                    print("16 bit overflow!")  # XXX
                    sample.make_16bit()
                self.output.play_sample(sample, async=True)
        sample = self.generate_sample(oscillator, 0.1)
        if sample:
            if sample.samplewidth != self.synth.samplewidth:
                print("16 bit overflow!")  # XXX
                sample.make_16bit()
            self.output.play_sample(sample, async=True)
            self.after_idle(lambda: self.continue_play_note(oscillator, False))

    def render_and_play_note(self, oscillator, max_duration=4):
        duration = 0
        for ev in self.envelope_filters:
            duration = max(duration, ev.duration)
        if duration == 0:
            duration = max_duration
        duration = min(duration, max_duration)
        sample = self.generate_sample(oscillator, duration)
        if sample:
            sample.fadein(0.05).fadeout(0.05)
            if sample.samplewidth != self.synth.samplewidth:
                print("16 bit overflow!")  # XXX
                sample.make_16bit()
            self.output.play_sample(sample, async=True)

    def stop_playing_note(self):
        self.playing_note = False
        to_speaker = [self.oscillators[i] for i in self.to_speaker_lb.curselection()]
        for osc in to_speaker:
            osc.set_title_status(None)
        self.keypress_sema.release()

    def pressed(self, event, note, octave, released=False):
        if self.arpeggio_playing:
            if not released:
                # arp still playing... stop it
                self.arpeggio_playing = False
            return
        a4freq = self.a4_choice.get()
        if self.arp_filter.input_mode.get().startswith("arp"):
            if released:
                self._pressed([0, 1, 2], released=True)
                return
            chord_keys = major_chord_keys(note, octave)
            if self.arp_filter.input_mode.get() == "arpeggio3":
                chord_keys = list(chord_keys)[:-1]
            chord_freqs = [note_freq(note, octave, a4freq) for note, octave in chord_keys]
            self.statusbar["text"] = "arpeggio: "+" ".join(note for note, octave in chord_keys)
            self.arpeggio_playing = True
            self._pressed(chord_freqs)
        else:
            self.statusbar["text"] = "ok"
            a4freq = self.a4_choice.get()
            freq = note_freq(note, octave, a4freq)
            self.current_note = (note, octave, freq)
            self._pressed(freq, released)

    def _pressed(self, freqs, released=False):
        # freqs can be a single frequency or a sequence of freqs (ARP)
        if isinstance(freqs, (tuple, list)):
            freq = freqs[0]
            arpeggio = True
        else:
            freq = freqs
            arpeggio = False
        if arpeggio and not self.arpeggio_playing:
            # stop the running arp cycle
            return
        to_speaker = [self.oscillators[i] for i in self.to_speaker_lb.curselection()]
        if not to_speaker:
            self.statusbar["text"] = "No oscillators connected to speaker output!"
            return
        if not arpeggio:
            if released:
                # only stop sound immediately if no echo filter is enabled
                if not self.echos_ending_time:
                    self.stop_playing_note()
                return
            if not self.keypress_sema.acquire(blocking=False):
                self.statusbar["text"] = "can't play new note - previous one still playing (monophonic, sorry)"
                return
        for osc in self.oscillators:
            if osc.input_freq_keys.get():
                osc.input_freq.set(freq*osc.input_freq_keys_ratio.get())
        for osc in to_speaker:
            if osc.input_waveformtype.get() == "linear":
                self.statusbar["text"] = "cannot output linear osc to speakers"
                return
            else:
                osc.set_title_status("TO SPEAKER")
        oscs = [self.create_osc(osc, self.oscillators, is_audio=True) for osc in to_speaker]
        mixed_osc = MixingFilter(*oscs) if len(oscs) > 1 else oscs[0]
        if not arpeggio:
            # at this time you can't use filters when using arpeggio
            mixed_osc = self.apply_filters(mixed_osc)
        current_echos_duration = getattr(mixed_osc, "echo_duration", 0)
        if current_echos_duration > 0:
            self.echos_ending_time = time.time() + current_echos_duration
        else:
            self.echos_ending_time = 0
        self.playing_note = True
        if arpeggio:
            # cycle the arp notes
            freqs.append(freqs[0])
            freqs = freqs[1:]
            rate = self.arp_filter.input_rate.get()
            duration = rate * self.arp_filter.input_ratio.get() / 100.0
            self.after_idle(lambda: self.render_and_play_note(iter(mixed_osc), max_duration=duration))
            self.after(int(rate*1000*0.95), lambda: self._pressed(freqs))
        else:
            # normal, single note
            self.output.wipe_queue()
            if self.rendering_choice.get() == "render":
                self.statusbar["text"] = "rendering note sample..."
                self.after_idle(lambda: self.render_and_play_note(iter(mixed_osc)))
                self.after_idle(lambda: self.stop_playing_note())
            else:
                self.after_idle(lambda: self.continue_play_note(iter(mixed_osc)))

    def apply_filters(self, output_oscillator):
        output_oscillator = self.tremolo_filter.filter(output_oscillator)
        output_oscillator = self.echo_filter.filter(output_oscillator)
        return output_oscillator

    def load_preset(self):
        file = askopenfile(filetypes=[("Synth presets", "*.ini")])
        cf = ConfigParser()
        cf.read_file(file)
        file.close()
        # general settings
        self.samplerate_choice.set(cf["settings"]["samplerate"])
        self.rendering_choice.set(cf["settings"]["rendering"])
        self.a4_choice.set(cf["settings"]["a4tuning"])
        self.to_speaker_lb.selection_clear(0, tk.END)
        to_speaker = cf["settings"]["to_speaker"]
        to_speaker = tuple(to_speaker.split(','))
        for o in to_speaker:
            self.to_speaker_lb.selection_set(int(o)-1)
        for section in cf.sections():
            if section.startswith("oscillator"):
                num = int(section.split('_')[1])-1
                osc = self.oscillators[num]
                for name, value in cf[section].items():
                    getattr(osc, name).set(value)
                osc.waveform_selected()
            elif section.startswith("envelope"):
                num = int(section.split('_')[1])-1
                env = self.envelope_filters[num]
                for name, value in cf[section].items():
                    getattr(env, name).set(value)
            elif section == "arpeggio":
                for name, value in cf[section].items():
                    getattr(self.arp_filter, name).set(value)
            elif section == "tremolo":
                for name, value in cf[section].items():
                    getattr(self.tremolo_filter, name).set(value)
            elif section == "echo":
                for name, value in cf[section].items():
                    getattr(self.echo_filter, name).set(value)
        self.statusbar["text"] = "preset loaded."

    def save_preset(self):
        file = asksaveasfile(filetypes=[("Synth presets", "*.ini")])
        cf = ConfigParser(dict_type=collections.OrderedDict)
        # general settings
        cf.add_section("settings")
        cf["settings"]["samplerate"] = str(self.samplerate_choice.get())
        cf["settings"]["rendering"] = self.rendering_choice.get()
        cf["settings"]["to_speaker"] = ",".join(str(v+1) for v in self.to_speaker_lb.curselection())
        cf["settings"]["a4tuning"] = str(self.a4_choice.get())
        # oscillators
        for num, osc in enumerate(self.oscillators, 1):
            section = "oscillator_"+str(num)
            cf.add_section(section)
            for name, var in vars(osc).items():
                if name.startswith("input_"):
                    cf[section][name] = str(var.get())
        # adsr envelopes
        for num, filter in enumerate(self.envelope_filters, 1):
            section = "envelope_"+str(num)
            cf.add_section(section)
            for name, var in vars(filter).items():
                if name.startswith("input_"):
                    cf[section][name] = str(var.get())
        # echo
        cf.add_section("echo")
        for name, var in vars(self.echo_filter).items():
            if name.startswith("input_"):
                cf["echo"][name] = str(var.get())
        # tremolo
        cf.add_section("tremolo")
        for name, var in vars(self.tremolo_filter).items():
            if name.startswith("input_"):
                cf["tremolo"][name] = str(var.get())
        # arpeggio
        cf.add_section("arpeggio")
        for name, var in vars(self.arp_filter).items():
            if name.startswith("input_"):
                cf["arpeggio"][name] = str(var.get())

        cf.write(file)
        file.close()