Direction 2 (Technical): Implement a DX7 synthesizer

Direction 2 (Technical): Implement a DX7 synthesizer#

The DX7 is a famous FM synthesizer made by Yamaha in the 1980s. It defines 32 algorithms that route 6 operators (sinusoidal oscillators with envelopes) in different configurations. Even with a fixed routin configurations, the timbre space is enormous: changing frequency ratios, modulation depths, and envelopes produces everything from electric pianos and brass to metallic bells and otherworldly textures.

In this direction, you will implement a DX7-style synthesizer from scratch: an operator primitive, two algorithms that each wire 6 operators together, two presets that produce meaningfully different timbres, and a meta-instrument that can sonify a MIDI file.

Steps#

There is no starter code for this assignment. Structure your implementation however you prefer, as long as it conforms to the requirements below.

  1. Implement an Operator. Write a function that represents a single DX7 operator. See the Operator Interface section below for the mathematical definition and required type signature.

  2. Wire up two algorithms. Pick two of the 32 DX7 algorithms (see the diagram below) and implement each by connecting 6 instances of your Operator according to the routing pattern. Avoid algorithms 4 and 6, which involve cross-operator feedback and are more complex to implement correctly.

  3. Define two presets. Each preset uses one of your two algorithms with a specific set of operator parameters (frequency ratios, output levels, envelopes) to produce a distinctive sound. Wrap each preset as an instrument function:

    def dx7_preset1(pitch: int, velocity: int, duration: float) -> pq.Audio
    def dx7_preset2(pitch: int, velocity: int, duration: float) -> pq.Audio
    

    where pitch is a MIDI note number, velocity is a MIDI velocity (0-127), and duration is in seconds. The two presets should produce meaningfully different timbres, not just the same sound at different volumes or pitches.

  4. Render single-note demos. Produce preset1.wav and preset2.wav (each a single note demonstrating the preset’s timbre).

  5. Sonify a MIDI file. Find a MIDI file of interest from BitMIDI. Define a meta-instrument function midi_to_dx7 that routes different MIDI program codes (instruments) to your two presets. Use it to render the MIDI score and produce song.wav:

    midi_score, midi_metronome = pq.midi_to_score("your_file.mid")
    song = midi_score.render(midi_to_dx7, midi_metronome)
    

    Your song.wav should be at most 60 seconds long. If the MIDI file is longer, trim the score or select a portion before rendering.

Operator Interface#

A DX7 operator uses phase modulation (the integrated form of frequency modulation). Your operator function must implement the following equation:

\[y[n] = \text{amp} \cdot \text{env}[n] \cdot \sin\!\Big(\underbrace{2\pi \cdot f_c \cdot \frac{n}{f_s}}_{\text{base phase}} + \underbrace{\text{mod}[n]}_{\text{mod input}} + \underbrace{\text{feedback} \cdot y[n-1]}_{\text{feedback}}\Big)\]

where \(N = \lfloor \text{duration} \cdot f_s \rfloor\) is the number of output samples and:

  • amp is the output amplitude. For modulators, this is the peak phase deviation in radians.

  • env is the amplitude envelope, a 1-D array of length \(N\).

  • f_c is the carrier frequency in Hz.

  • mod is the phase modulation input from another operator in radians, a 1-D array of length \(N\). Pass zeros when there is no modulation input.

  • feedback is the feedback scaling factor (0.0 when no feedback).

  • \(y[n-1]\) is this operator’s own previous output sample (0 for the first sample).

DX-7 Operator

Implement the operator as a function with this signature:

def operator(
    amp: float,
    env: np.ndarray,
    f_c: float,
    mod: np.ndarray,
    duration: float,
    feedback: float = 0.0,
    f_s: int = 44100,
) -> pq.Audio

DX7 Algorithms#

Each algorithm wires 6 operators into a specific routing. An adjoining line means signal addition. An incoming line to an operator means phase modulation (chaining). The looping arrow on some operators indicates self-modulation (see below).

DX-7 Algorithms

For example, in Algorithm 8: Operators 1 and 3 (the carriers) are summed to form the output. Operator 2 modulates Operator 1. Operators 4 and 5 are summed to modulate Operator 3. Operator 6 modulates Operator 5. The loop on Operator 4 indicates self-modulation.

Avoid algorithms 4 and 6, which involve feedback between different operators (cross-operator feedback). All other algorithms are allowed.

Self-Modulation#

Some DX7 algorithms include an operator that modulates itself, shown as a looping arrow in the diagram. To implement self-modulation, pass a non-zero feedback value to your operator function. Because \(y[n]\) depends on \(y[n-1]\), you must compute self-modulating operators sample-by-sample with a for-loop rather than using vectorized NumPy operations.

Requirements#

Each requirement is worth the points listed. These 80 points are the manually graded part of your grade. The other 20 come from the Gradescope formatting check.

Requirement

Points

You implement an operator function matching the signature and equation described in the Operator Interface section.

12

You implement two DX7 algorithms (one per preset) by wiring 6 Operator instances according to the DX7 routing diagrams. Algorithms 4 and 6 are excluded (cross-operator feedback). Each algorithm is worth 8 points.

16

You define two preset instrument functions with the signature (pitch: int, velocity: int, duration: float) -> pq.Audio, where pitch is a MIDI note number, velocity is a MIDI velocity (0-127), and duration is in seconds. Each preset uses a different algorithm.

8

The two presets produce meaningfully different timbres, not just different pitches or volumes.

6

velocity affects output level: higher velocity produces louder output.

4

pitch controls the fundamental: calling a preset at two different pitches produces sounds with correspondingly different fundamental frequencies. The pitch cannot be hard-coded.

5

You define a midi_to_dx7 meta-instrument that routes different MIDI programs to your two presets.

8

preset1.wav is a single note rendered by dx7_preset1.

3

preset2.wav is a single note rendered by dx7_preset2.

3

song.wav is produced by rendering a MIDI score with midi_to_dx7. It is at most 60 seconds long.

6

TECHNICAL.md documents your algorithm choices, operator implementation, the parameter values for each preset, and how midi_to_dx7 routes programs to presets.

9

Total

80

Bonus#

Parse a real DX7 patch from a SysEx (.syx) file and use its parameters to configure your synthesizer. Original DX7 patches are available at Yamaha Black Boxes. You can compare your output sound with this web-based emulation for accuracy. Document which patch you recreated and how closely your output matches in TECHNICAL.md.

Submission#

Your submission must conform to the technical format. Bundle the following into a single .zip file (under 100MB) and submit it via Gradescope.

  1. demo.mp4, demo.mov, or demo.mkv: a 120-300 second screen recording, narrated in your own voice, explaining your approach and demonstrating your result. See the technical format for the required presentation flow.

  2. TECHNICAL.md: use the open-ended template and leave all ## Section Headings unchanged. Your write-up must state:

    • Which two DX7 algorithms you implemented and why you chose them.

    • How you implemented your operator function.

    • The parameter values for each of your two presets (frequency ratios, output levels, envelope settings) and how you chose them.

    • How your midi_to_dx7 routes MIDI programs to presets.

    • If you attempted the bonus: which SysEx patch you recreated and how it compares to the original.

  3. src/: your Python/Pyquist code plus any sound assets it needs. Anything we can pip install does not need to be included. At minimum this directory should contain something like:

    • dx7.ipynb or dx7.py: your Operator implementation, algorithms, presets, and MIDI rendering.

    • preset1.wav: a single note from your first preset.

    • preset2.wav: a single note from your second preset.

    • song.wav: a MIDI file rendered with your presets.

Note

The Gradescope autograder only checks the top-level structure of your zip file. It does not verify the specific contents of your src/ directory. Making sure the files above are present, complete, and runnable is up to you.