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.
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)
my_array[start:end, start:end] for 2D arrays).
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
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], ']')
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]]
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])
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
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()) 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)
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]
.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]]
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]]
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]]]
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 is a powerful feature that enables NumPy to handle arrays of varying shapes during arithmetic operations. Broadcasting two arrays together follows these rules:
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.
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.
Duration (seconds) = Total Number of Samples / Sampling Rate.
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.
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.
This function controls the volume of the audio signal. The process consists of several standard steps in digital signal processing:
adjust_factor.
.astype()) to maintain the correct format.
A Noise Gate automatically mutes the audio when it is quieter than a certain level.
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.
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.
Example (with kernel size = 49):
Before Noise Reduction (After Noise Gate):
After Noise Reduction:
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.
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: a 2D NumPy array with left and right channels
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.
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.
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.
lab9.py to implement Trim Audio, Adjust Volume,
Noise Gate and Noise Reduction.
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.
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):
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.
adjust_factor.numpy.clip(): https://numpy.org/doc/stable/reference/generated/numpy.clip.html
numpy.iinfo(): https://numpy.org/doc/stable/reference/generated/numpy.iinfo.html
numpy.ndarray.astype(): https://numpy.org/doc/stable/reference/generated/numpy.ndarray.astype.html
Example:
Before Adjust Volume (Original):
After Adjust Volume (1.5x):
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.)
numpy.iinfo(): https://numpy.org/doc/stable/reference/generated/numpy.iinfo.html
threshold_amplitude from open_threshold_db.numpy.abs(): https://numpy.org/doc/2.2/reference/generated/numpy.absolute.html
Example:
Before Noise Gate (with open threshold dB = -30):
After Noise Gate:
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.)
kernel_size (use integer
division
//).
numpy.pad() with mode='edge': https://numpy.org/doc/stable/reference/generated/numpy.pad.html((pad, pad), (0, 0)).
numpy.zeros_like(): https://numpy.org/doc/stable/reference/generated/numpy.zeros_like.html
numpy.mean(): https://numpy.org/doc/stable/reference/generated/numpy.mean.html
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:
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 labtest_audio/ - Folder containing audio files for testing your implementationtest_cases/ - Folder containing input and expected output files for testingpip install -r requirement.txt to install all required dependencies
lab9.py as specified above.audio_cli.py or audio_gui.py on the
provided audio: audio_original.wavlab9_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
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
diff -u test_cases/output01.txt myOutput01.txt
(Windows: you can open both files and compare, or use any file-compare tool.)
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.
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.pyYou 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.
audio_gui.py to cater for some devices not being able to handle the
sounddevice library.
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.