COMP 1023 Introduction to Python Programming

Lab 9 (Very Simplified) Audio Processing with Numpy

Review

Welcome to the ninth lab of COMP 1023. In this review section, we will review the fundamentals of NumPy.

NumPy stands for Numerical Python and serves as the fundamental library for numerical and scientific computing in Python. It offers a high-efficiency multidimensional array structure along with various functions for manipulating them. A NumPy array represents a grid of values that are all of the same data type.

Terminology

  • Rank: The number of dimensions in the array.
  • Shape: A tuple of integers that specifies the size of the array in each dimension.

Creating NumPy Arrays

NumPy arrays can be created via lists and different NumPy functions:
import numpy as np
a: np.ndarray = np.array([10, 20, 30]) # Create a 1D NumPy array using a list
b: np.ndarray = np.zeros((3,3))        # Create a 2D NumPy array with 3 rows and 3 columns filled with zeros
c: np.ndarray = np.ones((2,3))         # Create a 2D NumPy array with 2 rows and 3 columns filled with ones
d: np.ndarray = np.full((3,2), 5)      # Create a 2D NumPy array with 3 rows and 2 columns filled with a constant value 5
e: np.ndarray = np.eye(3)              # Create a 3x3 identity matrix (ones on the diagonal, zeros elsewhere)

Indexing and Slicing Arrays

An element in a NumPy array can be accessed by its index using square brackets. NumPy arrays can also be sliced in the same way as Python lists, but they can be sliced in multiple dimensions (e.g., my_array[start:end, start:end] for 2D arrays).

Integer indexing can be combined with slice indexing; however, this will result in an array of lower rank compared to the original array.
import numpy as np
a: np.ndarray = np.array([[11, 12, 13, 14], [15, 16, 17, 18], [19, 20, 21, 22]])
b: np.ndarray = a[2, :]   # Rank 1 view of the 3rd row of a
c: np.ndarray = a[2:3, :] # Rank 2 view of the 3rd row of a

Integer Array Indexing and Boolean Array Indexing

Integer array indexing enables us to create arbitrary arrays using data from another array. It is also called Fancy Indexing or Advanced Indexing.
import numpy as np
a: np.ndarray = np.array([[7, 8], [9, 10], [11, 12]])
print(a[[0, 1, 2], [1, 0, 1]]) # Output: [8 9 12], equivalent to print('[', a[0, 1], a[1, 0], a[2, 1], ']')

We can also use integer indexing to select or modify one element from each row of an array.
import numpy as np
a: np.ndarray = np.array([[13, 14, 15], [16, 17, 18], [19, 20, 21], [22, 23, 24]])
b: np.ndarray = np.array([1, 0, 2, 1])
print(a[np.arange(4), b]) # Output: [14 16 21 23]
a[np.arange(4), b] += 5
print(a)   # Output: [[13 19 15]
           #          [21 17 18]
           #          [19 20 26]
           #          [22 28 24]]

Boolean array indexing allows us to select arbitrary elements from an array. This type of indexing is often used to extract elements that meet a specific condition. It is also referred to as Fancy Indexing or Advanced Indexing.
import numpy as np
a: np.ndarray = np.array([[7, 8], [9, 10], [11, 12]])
bool_idx: np.ndarray = (a < 10)
print(bool_idx)    # Output: [[ True  True]
                   #          [ True False]
                   #          [False False]]
print(a[bool_idx]) # Output: [7 8 9], or equivalently print(a[a < 10])			   

Data Types in NumPy

Each NumPy array consists of a grid of elements of the same type. When we create an array, NumPy attempts to determine the appropriate data type, but we can explicitly specify the data type using an optional parameter, dtype.
import numpy as np
a: np.ndarray = np.array([3.5, 4.5])
print(a.dtype)  # Output: float64
b: np.ndarray = np.array([3, 4], dtype=np.float32)
print(b.dtype)  # Output: float32

Mathematical Operations on Arrays

We can perform mathematical operations using operators (e.g., +, -, *, /) and functions.
import numpy as np
a: np.ndarray = np.array([[2, 3], [4, 5]], dtype=np.float64)
b: np.ndarray = np.array([[6, 7], [8, 9]], dtype=np.float64)
print(a + b)  # Equivalent to print(np.add(a, b))

Dot Product and Matrix Multiplication

The dot product (.dot()) is defined as the sum of the products of corresponding elements in two arrays of the same size. The result of the dot product is a single scalar value. Matrix multiplication is an operation where each element corresponds to the dot product of row/column pairs from the two 2D arrays.
import numpy as np
x: np.ndarray = np.array([[2, 4], [6, 8]])
y: np.ndarray = np.array([[1, 3], [5, 7]])
v: np.ndarray = np.array([8, 9])
w: np.ndarray = np.array([10, 11])
print(v.dot(w)) # Or equivalently, print(np.dot(v, w)), print(np.matmul(v, w)), print(v@w)
print(x.dot(v)) # Or equivalently, print(np.dot(x, v)), print(np.matmul(x, v)), print(x@v)
print(x.dot(y)) # Or equivalently, print(np.dot(x, y)), print(np.matmul(x, y)), print(x@y)

Functions in NumPy

  • Mean
    The numpy.mean() function computes the arithmetic mean (average) of elements in a NumPy array. It can calculate the mean of all elements in a flattened array, or along a specified axis in a multi-dimensional array.
    import numpy as np
    a: np.ndarray = np.array([[5, 10], [15, 20]])
    print(np.mean(a))         # Calculate the mean of all elements; output 12.5
    print(np.mean(a, axis=0)) # Calculate the mean of each column; output [10. 15.]
    print(np.mean(a, axis=1)) # Calculate the mean of each row; output [7.5 17.5]
    

  • Transpose
    Transposing an array (.T attribute, numpy.transpose(), ndarray.transpose()) involves reordering its axis.
    import numpy as np
    a: np.ndarray = np.array([[5, 6], [7, 8]])
    print(a.T)  # Output [[5 7] , or equivalently, print(np.transpose(a)), print(a.transpose())
                #         [6 8]]
    

  • Reshape
    Reshaping a NumPy array using ndarray.reshape(new_shape) involves changing its dimensions without altering the underlying data.
    import numpy as np
    a: np.ndarray = np.array([10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120])
    b = a.reshape(4, 3) # Reshape a into a 2D array with 4 rows and 3 columns
    print(b)            # Output [[ 10  20  30]
                        #         [ 40  50  60]
    					#         [ 70  80  90]
    					#         [100 110 120]]
    

  • Newaxis and Expand_dims
    Both numpy.newaxis and numpy.expand_dims are used to increase the dimensionality of an array by adding a new axis.
    import numpy as np
    a: np.ndarray = np.array([[5, 6, 7], [8, 9, 10]])
    print(a)  # Output: [[ 5  6  7]
              #          [ 8  9 10]]
    b: np.ndarray = a[:, np.newaxis, :]  # Equivalent to b = a[:, None, :]
                                         # Also, equivalent to b = np.expand_dims(a, axis = 1)
    print(b)  # Output: [[[ 5  6  7]]
              #          [[ 8  9 10]]]
    

View and Copy

A view is a reference to the original data, and a copy creates a new object with the same data. Changes made through a view affect the original data, but changes made to a copy do not affect the original object.

Broadcasting

Broadcasting is a powerful feature that enables NumPy to handle arrays of varying shapes during arithmetic operations. Broadcasting two arrays together follows these rules:

  1. If the arrays differ in rank, prepend 1s to the shape of the lower rank array until both shapes are of equal length.
  2. Two arrays are considered compatible in a dimension if they have the same size in that dimension, or if one of the arrays has a size of 1 in that dimension.
  3. The arrays can be broadcast together if they are compatible in all dimensions.

Put an image in images/, link it and change the alt text
Photo by WONG, Lap Ming on 23rd Mar 2026

Introduction

In this lab, you will do very simple audio processing with Numpy using provided Python scripts: lab9.py, and test your implementation at audio_cli.py for command-line operations and audio_gui.py for graphical user interface operations. The main tasks include trim audio, adjust volume, noise gate and noise reduction, which are described in the following.

Fundamental of Audio Processing

Before diving into the specific tasks, it is essential to understand some fundamental concepts in audio processing.

How Audio Works:
Sound is physically generated by vibrations traveling through a medium like air as continuous pressure waves. When these waves hit a microphone, they are converted into continuous electrical signals (analog). To process sound on a computer, this continuous signal must be converted into a digital format. This is done by measuring the height (amplitude) of the wave at regular intervals, a process called sampling.

Sampling Process Visualization
How digital audio data sample from graph

Therefore, digital audio is essentially a long sequence of numbers (samples), where each number represents the air pressure at a specific tiny moment in time.

  • Sampling Rate: Sound occurs naturally as a continuous wave. To capture it digitally, we measure the wave's height at extremely fast, regular intervals—a process called sampling. The Sampling Rate dictates how many of these measurements (samples) are taken per second. For example, a rate of 44,100 Hz means the computer recorded the sound's amplitude 44,100 times in just one second. High sampling rates capture more detail, resulting in "smoother" audio that closely resembles the original wave. This rate also links the array index to real-world time, allowing us to calculate duration: Duration (seconds) = Total Number of Samples / Sampling Rate.

  • Bit Depth: While sampling rate handles the timing, Bit Depth controls the precision of each sample's value. It acts as the "vertical" resolution of the audio waveform. A higher bit depth allows the computer to measure the sound's loudness with much finer accuracy. For instance, in 16-bit audio (like a CD), every sample is a number between -32,768 and 32,767. More bits mean a wider dynamic range and less digital noise, allowing for both whisper-quiet sounds and loud booms to be captured faithfully without distortion.

  • Amplitude (Loudness): This is the strength, intensity, or magnitude of the audio signal at any specific moment in time. In a digital audio file, amplitude is represented by the numerical value of each sample. Audio waves oscillate around zero, so the values swing between positive and negative numbers.
    • Magnitude: The "loudness" corresponds to the absolute value (distance from zero). Larger numbers mean louder sound. Zero means silence.
    • Bit Depth: The range of possible amplitude values is determined by the "bit depth". For example, 16-bit audio allows values between -32,768 and +32,767.

  • Decibel (dB): Our ears perceive loudness logarithmically, not linearly. The Decibel (dB) is a unit used to express this relative loudness. In digital audio systems, the scale is typically defined relative to the maximum possible amplitude:
    • Highest (0 dB): This represents the maximum possible loudness. It is the "ceiling" of the digital audio format. Any signal attempting to go above 0 dB will result in distortion (clipping).
    • Lowest (-∞ dB): Negative infinity represents absolute silence (zero amplitude).
    • Typical Range: Most audible sounds in a digital file will range from negative values (e.g., -60 dB for quiet background noise) up to close to 0 dB for the loudest peaks.
    In audio processing, a change in decibels relates to the change in amplitude. For example, increasing volume by roughly 6 dB corresponds to doubling the amplitude.
    • To convert Amplitude to dB: dB = 20 * log10(Amplitude)
    • To convert dB to Amplitude: Amplitude = 10^(dB/20)


Now, let's talk about the audio processing routines that you need to implement in Lab 9. They are Trim Audio, Adjust Audio, Noise Gate and Noise Reduction.

Trim Audio

The Trim Audio function allows you to slice the audio to keep only the segment between a specified start and end time index.

Concept: Since digital audio is just a sequence of samples occurring over time, "cutting" audio is as simple as selecting a sub-sequence of these numbers. For example, if you have 10 seconds of audio and you only want the middle 2 seconds, you determine which sample numbers correspond to the start and end of that 2-second block and discard the rest.

This is commonly used to remove technical silence at the beginning of a recording, cut out applause at the end, or isolate a specific spoken word from a sentence.

Trim Audio Visualization
Trim Audio illustration from 2.2 seconds to 4.6 seconds

Adjust Audio

This function controls the volume of the audio signal. The process consists of several standard steps in digital signal processing:

  • Scaling: Since amplitude corresponds to loudness, we change the volume by multiplying every sample in the sequence by a constant number called the adjust_factor.
    • If factor > 1.0, the numbers get bigger (LOUDER).
    • If factor < 1.0, the numbers get smaller (quieter).
  • Clipping: There is a maximum limit to how loud digital audio can be (the "ceiling"). If you amplify a signal too much, the values might try to go above this limit. Since the computer cannot store numbers larger than the limit, we must "clip" them—forcing any value that is too high to equal the maximum limit, and any value too low to equal the minimum limit. Without this safety step, the numbers would "overflow" and wrap around, causing horrible digital noise.
  • Type Casting: Since audio data is typically stored as integers, processing often creates floating-point numbers. Before returning the final audio array, you must convert these values back to the original integer data type (using .astype()) to maintain the correct format.
Trim Audio Visualization
Adjust volume illustration for a factor > 1.0

(Simplified) Noise Gate

A Noise Gate automatically mutes the audio when it is quieter than a certain level.

  • Threshold Calculation: The user provides a threshold in decibels (dB), which represents the "floor" level. Any sound quieter than this floor is assumed to be unwanted background noise (like a computer fan or AC hum).
  • Masking: The algorithm scans the entire audio sequence. It compares the absolute magnitude of each sample against the calculated threshold.
  • Gating:
    • If a sample is louder than the threshold, the gate stays "open" and the sound passes through unchanged.
    • If a sample is quieter than the threshold, the gate "closes" and the value is rigorously set to 0 (absolute silence).
    This creates a "cleaner" sounding recording by removing the hiss from the silent pauses between words or notes.
Noise Gate Visualization
Noise Gate illustration (with open threshold dB = -30)

Example (with open threshold dB = -30):
Before Noise Gate:

After Noise Gate:

You may notice that after applying the noise gate, the audio has sudden drops to silence which can sound unnatural (Since we are just using a simplifed noise gate, if you want to know more about noise gate, you can refer to here, but we are not going to implement this noise gate in this lab). Therefore, we will use Noise Reduction to smooth out these transitions.

Noise Reduction (Moving Average Filter)

This function attempts to remove high-frequency background noise (like static hiss) using a smoothing technique involving a Moving Average Filter. It can also help resolve sudden "drops" or artifacts that might occur after applying a hard Noise Gate, making the transitions between silence and sound less jarring.

  • Moving Average Filter: The filter works by "smoothing" the audio signal.
    • Kernel Size: The number of samples included in this average is called the Kernel Size (or window size).
    • Padding (Boundary Handling): At the very start and end of the file, we run out of neighbors to average. To fix this, we artificially extend (pad) the edges of the signal so the math still works. This is the Padding process, where we add extra samples (based on half the kernel size) to the beginning and end of the audio array.
    • Sliding Window: Imagine looking at the waveform through a small sliding window. For every sample point, we look at its neighbors (e.g., 5 samples before it and 5 samples after it).
    • Calculate Average: We calculate the average value of the samples inside this window. This average value becomes the new value for that point in time.
    Moving Average Filter Visualization
    Moving Average Filter in Audio Signal Illustration (with Kernel size 3)
  • Smoothing Effect: Random noise tends to jump up and down very quickly (high frequency). Averaging these jitters tends to cancel them out, resulting in a smooth line. The meaningful audio signal usually changes more slowly, so it is largely preserved by the averaging.
  • Trade-off: While this reduces noise, it can also smooth out the sharp "edges" of the sound we want to keep (like the sharp attack of a drum hit), potentially making the audio sound slightly "muffled".
Noise Reduction Visualization
Noise reduction illustration (with kernel size = 49)

Example (with kernel size = 49):
Before Noise Reduction (After Noise Gate):

After Noise Reduction:

Audio Data Representation

Before diving into the tasks, it is important to understand how audio signals are represented as NumPy arrays in this lab. All four tasks operate on the same underlying data structure, so a clear mental model here will make the implementation much easier.

Shape

An audio signal is stored as a 2D NumPy array with shape (num_samples, num_channels). For the stereo audio used in this lab, num_channels = 2, corresponding to the left and right channels. For example, a 2-second stereo clip sampled at 48,000 Hz has shape (96000, 2).

Audio Signal Visualization
Visualization of audio_signal: a 2D NumPy array with left and right channels
Indexing

Because the array is 2D, indexing returns per-sample pairs rather than scalars:

  • audio_signal[i] → the i-th sample across all channels, e.g. a length-2 array [left, right] for stereo.
  • audio_signal[i][0] → the i-th sample of the left channel.
  • audio_signal[i][1] → the i-th sample of the right channel.
  • audio_signal[:, 0] → the entire left channel as a 1D array.
  • audio_signal[start:end] → a slice of samples from start to end across both channels.

When you apply operations like padding, mean, or boolean masks, remember to specify axis=0 if you want to operate along the time dimension while preserving the two channels.

Data Type

Samples are stored as int16 (16-bit signed integers), so values range from -32,768 to 32,767. Each integer represents the amplitude of the sound wave at that point in time. When you perform arithmetic on the array, the result may temporarily exceed this range or change to a floating-point dtype, so be mindful of clipping values back into the valid int16 range and casting the array back to its original dtype when needed.

Sampling Rate

The sampling rate (e.g. 48,000 Hz) is the number of samples that represent one second of audio. This means array index and time are directly related: time_in_seconds = index / sampling_rate. So to extract the first 2 seconds of a 48,000 Hz clip, you would slice from index 0 to index 2 * 48000 = 96000.

Lab Work

Your task is to complete the code in lab9.py to implement Trim Audio, Adjust Volume, Noise Gate and Noise Reduction.

⚠️ Warning: Volume Safety

When testing your audio implementations, please lower the volume on your speakers or headphones before running the tests. If your implementation is incorrect, it may produce unexpectedly loud, distorted, or harsh audio that could damage your hearing. Start with a low volume and gradually increase it as you verify your code is working correctly.


Task 1 - Implement Trim Audio

Implement the trim_audio function to slice the audio array to keep only the segment between the start and end indices. (See Trim Audio explanation in the Introduction section.)

The conversion from seconds to frame indices and the clipping to valid range are already handled by the Audio class before calling your function. You only need to perform the slicing on the given indices, and you can assume that start_index and end_index will always be within the valid range. You are not allowed to use any loops for this task.

Example:
Before Trim (Original):

After Trim (1s to 4s):

Task 2 - Implement Adjust Volume

Implement the adjust_volume function to modify the volume of the audio signal based on a specified adjustment factor as outlined in the Adjust Audio section. You may also need to understand Amplitude and Decibel concepts.

  1. Multiply the audio signal by the adjust_factor.
  2. Clip the formatted values to fit within the valid range of the original data type
    Hint 1: Use numpy.clip(): https://numpy.org/doc/stable/reference/generated/numpy.clip.html
    Hint 2: Use numpy.iinfo(): https://numpy.org/doc/stable/reference/generated/numpy.iinfo.html
  3. Cast the result back to the original data type (e.g., from float back to int16).
    Hint: Use numpy.ndarray.astype(): https://numpy.org/doc/stable/reference/generated/numpy.ndarray.astype.html
  4. Return the processed signal.
Note: You are not allowed to use any loops for this task.

Example:
Before Adjust Volume (Original):

After Adjust Volume (1.5x):

Task 3 - Implement (Simplified) Noise Gate

Implement the noise_gate function to mute the audio when it falls below a certain threshold. (See Noise Gate explanation in the Introduction section. You will also need Amplitude and Decibel concepts.)

  1. Determine the maximum possible amplitude for the audio data type (e.g., 32767 for 16-bit audio).
    Hint: Use numpy.iinfo(): https://numpy.org/doc/stable/reference/generated/numpy.iinfo.html
  2. Calculate the linear threshold_amplitude from open_threshold_db.
    Formula: Amplitude = MaxAmplitude * 10^(dB / 20)
  3. Create a boolean mask where the absolute amplitude of the signal is greater than this threshold.
    Hint: Use numpy.abs(): https://numpy.org/doc/2.2/reference/generated/numpy.absolute.html
  4. Apply the mask to set samples below the threshold to 0.
    Note: You can use boolean indexing or simply multiply the signal by the mask.
  5. Return the processed signal.
Note: You are not allowed to use any loops for this task.

Example:
Before Noise Gate (with open threshold dB = -30):

After Noise Gate:

Task 4 - Implement Noise Reduction (Moving Average Filter)

Implement the noise_reduction function to smooth the audio signal using a moving average filter. You can always assume the kernel size will always be an odd number. (See Noise Reduction explanation in the Introduction section.)

  1. Calculate the padding amount, which is half of the kernel_size (use integer division //).
  2. Pad the signal array using edge values.
    Hint: Use numpy.pad() with mode='edge': https://numpy.org/doc/stable/reference/generated/numpy.pad.html
    Note: Pad only the first dimension (time), not the second dimension (channels, if any). The padding tuple should look like ((pad, pad), (0, 0)).
  3. Initialize an output array of zeros with the same shape as the input.
    Hint: Use numpy.zeros_like(): https://numpy.org/doc/stable/reference/generated/numpy.zeros_like.html
  4. Apply the moving average filter:
  5. Return the smoothed signal cast back to the original data type.
    Hint: Use numpy.ndarray.astype(): https://numpy.org/doc/stable/reference/generated/numpy.ndarray.astype.html.

Example:
Before Noise Reduction (After Noise Gate, with kernel size = 49):

After Noise Reduction:

Resources & Sample I/O

How to Start

  1. Download the complete lab package: lab9_skeleton.zip.
  2. You will see:
    • lab9.py - Your work file (implement TODOs here)
    • audio.py - The Audio Class which help manipulate audio wave data from WAV files (DO NOT MODIFY)
    • audio_cli.py - The console testing application (DO NOT MODIFY)
    • audio_gui.py - The Graphical User Interface (GUI) testing application (DO NOT MODIFY)
    • requirement.txt - A list of library which required to install in this lab
    • test_audio/ - Folder containing audio files for testing your implementation
    • test_cases/ - Folder containing input and expected output files for testing
  3. You can open the virtual environment and setup required package as follow:
    • Remember: Open your virtual environment and run pip install -r requirement.txt to install all required dependencies
    • For help on creating a virtual environment, refer to the setup guide
    • Or you can follow this video to open it:
  4. Implement the tasks in lab9.py as specified above.
  5. Test your implementation by running audio_cli.py or audio_gui.py on the provided audio: audio_original.wav

File Structure

lab9_skeleton/
├── test_audio
│   ├─ audio_original.wav           ← The audio for testing 
│   ├─ audio_after_task1.wav        ← The result audio after process Task 1
│   ├─ audio_after_task2.wav        ← The result audio after process Task 2
│   ├─ audio_after_task3_and_4.wav  ← The result audio after process Task 3, then Task 4
│   ├─ audio_after_task3.wav        ← The result audio after process Task 3
│   └─ audio_after_task4.wav        ← The result audio after process Task 4
├── test_cases
│   ├─ input0X.txt                  ← Input file for test case
│   └─ output0X.txt                 ← Expected output for the corresponding input
├── lab9.py                         ← Your implementation (Tasks 1-4)
├── audio.py                        ← Audio Class help manipulate audio wave data from WAV files.
├── audio_cli.py                    ← Console testing application (DO NOT MODIFY)
├── audio_gui.py                    ← GUI testing application (DO NOT MODIFY)
└── requirement.txt                 ← Help to setup library use in this lab

How to Run & Test

This lab provides a set of public test cases and audio file to help you check your program. Each test case comes as a pair of files: inputXX.txt (what the user types) and outputXX.txt (the expected printed output).

Option 1: Text Mode (Used for self-testing & grading)
Run python audio_cli.py. This runs the CLI version. You can also run a test case automatically using input/output redirection:

  • python audio_cli.py < test_cases/input01.txt > myOutput01.txt
  • Compare your output with the expected one: diff -u test_cases/output01.txt myOutput01.txt (Windows: you can open both files and compare, or use any file-compare tool.)
  • To run all test cases at once (Linux/Mac):
    for i in {1..5}; do
        python audio_cli.py < test_cases/input0$i.txt > myOutput0$i.txt
        diff -u test_cases/output0$i.txt myOutput0$i.txt
    done

Option 2: GUI Mode (For fun)
Run python audio_gui.py. This launches the visual interface shown in class.

Submission & Deadline

The lab assignment is due on 4th May 2026, 23:59. We will use the online grading system ZINC to grade your lab work. You are required to upload a folder/directory containing the following files to ZINC:

  • lab9.py

You can submit your code to ZINC as many times as you like before the deadline. Only your LAST submission will be graded. After the due date, we will regrade your work using hidden test cases. We do this to ensure that students don't just hardcode the answers, like printing the correct outputs without really solving the problems. The hidden test cases will be similar in difficulty to the provided test cases but will use different inputs (this might not apply to the programming assignment). Also, please keep in mind that getting full marks with the provided test cases before the deadline doesn't guarantee you will get full marks with the hidden test cases after the deadline, since the inputs will be different.

Changelog

  • 2026-04-24:
    • Updated audio_gui.py to cater for some devices not being able to handle the sounddevice library.
  • 2026-04-28:
    • Add description on how to setup virtual environment.
  • 2026-05-03:
    • Extended deadline to May 4th.

Frequently Asked Questions

My code doesn't work / there is an error, here is the code. Can you help me fix it?

As the assignment is a major course assessment, to be fair, we should not finish the tasks for you.
We might provide you with some hints, but we won't debug for you.

Page maintained by
Homepage