A. Audio processing
Audio processing was performed using MATLAB (R2020b; TheMathWorks, Inc, MA, USA). The audio source was an MP3 file with a sampling rate of 48kHz, bit rate of 192 kbps, and length 2 min 40 s. All processing was performed using in-built MATLAB functions. Firstly, both channels of audio (stereo) were combined into a single vector. A fast Fourier transform (‘fft.m’) was then used to convert this vector into frequency space and the real component of the data was isolated. Three new vectors were then produced, which isolated out three frequency bands: low frequency (‘sub-base’ to ‘lower midrange’; 16Hz-500Hz), ‘midrange’ (500Hz-2kHz), and high frequency (‘higher midrange’ to ‘brilliance’; 2kHz-20kHz). For each new vector all frequencies outside of these ranges were set to zero. An inverse fast Fourier transform (‘ifft.m’) was then used to convert the three vectors back into time signals. A quadratic regression smoothing function (‘smoothdata.m’) was then applied, with a window of 48000 (corresponding to 1 s for 48kHz data). All three vectors were then decimated to 60Hz and normalized to produce cartesian coordinates (x, y, and z), which were exported as a CSV file (along with a time vector) for use in the crystal lattice simulation.
B. Rendering a crystal lattice – Atoms and bonds
The rendering software used for this project was Trinity Visualisation Suite 2.0 (TVS2). This software was developed by Research IT, formerly known as the Trinity Centre for High Performance Computing (TCHPC; TCD, Dublin, Ireland) as part of the Institute for Information Technology and Advanced Computing (IITAC) project, with the goal of being able to visualize VASP files for the Trinity Computational Chemistry group. Over the years, additional functionality has been added to it, which makes it particularly well suited for adding custom features such as the ones examined in this project.
In this software, the rendering of a VASP dataset is based on the image of a single lattice cell. The dataset provides information about some of the atoms in the original cuboid, and it is up to the software to figure out which ones will be in the border of the same cell as it considers the neighbouring lattices. The bonds are then created by setting a distance interval that is larger than the Van Der Waals radii but is small enough that any pair of atoms within that distance must be bonded by either a covalent or Hydrogen bond.
C. Atoms moving independently
The atoms that were rendered can be, and often are, located at the boundary between two cells, so technically they belong to both (potentially, up to eight cells, if they are in one of the top or bottom corners of the lattice). Because of this, it was not possible to use a discrete number of copies of the lattice in each direction when choosing the magnitude of the movement. Instead, the distance to the original cell was used, so the distortion or perturbation became smaller as atoms were further away from the initial cell. Each one of these moved independently, which meant that all the atoms in the original cell moved with the original perturbation, with no scale or timestep distortion (i.e. all moving the same distance proposed by the current timestep), whereas the movement of atoms outside the original cell were affected by both perturbation and distortion. That way, the further away that an atom was from the original cell, the smaller the perturbation and the larger the distortion that was applied to it would be.
D. Bonds moving
Bonds were considered as links between two atoms within a predetermined distance interval. Thus, the movement of the bonds was determined by the movements of the two atoms that it linked. We acknowledge that this approach risks not preserving scientific accuracy - as both the perturbations and distortions applied to them could potentially allow a pair of atoms to move away from each other beyond the distance that would determine a chemical bond between them. However, for the sake of maintaining the crystal structure of the hypothetical material used for this visualization, bonds that were calculated from the unperturbed, undistorted crystal structure were retained throughout the animation, regardless of the distance between atoms.
E. Moving a single cell
All atoms in the original cell were perturbed by the amount determined by the CSV file extracted from the input audio data, which contained four columns (timesteps and x, y and z displacements), see Table 1.
Table 1
TVS2 code for algorithm 1.
Algorithm 1: Draw original grid atoms and bonds |
---|
XYZ atomPosition = atomPositionthisAtom; XYZ perturbation = perturbationstimeStep; for Every atom in the original grid do glTranslatef(+ atomPosition + perturbation); drawAtom(); glTranslatef(-atomPosition-perturbation); end for for Every bond in the original grid do aPoint = atomPosition [ bondsbondId.atomA() ] + perturbation; bPoint = atomPosition [ bondsbondId.atomB() ] + perturbation; if atom Type of [bonds[ui].getX()] is different to atom Type of [bonds[ui].getY()]) then midPoint = (aPoint + bPoint)*0.5; setColorToAtom(bondsbondId.atomA()) drawBond(aPoint,midPoint); setColorToAtom(bondsbondId.atomB()) drawBond(midPoint,bPoint); else setColorToAtom(bondsbondId.atomA()) drawBond(aPoint,bPoint); end if end for |
F. Propagation along multiple cells - Space
Next, the propagation of the perturbation to atoms in other cells were calculated. In this case, the perturbation was reduced compared to the original cell as a result of distance from the original cell. Since, as previously mentioned, an atom can belong to more than one cell, two arrays were created, atomsDistance and atomsWeight. In atomsDistance the distance of each atom that was rendered to the original cell was stored. In atomsWeight, the weight to be applied to each atom was stored, depending on their corresponding value in atomsDistance (Table 2). These weights were then used to scale the perturbation of a particular atom, allowing for a smooth transition between cells (Table 3).
Table 2
TVS2 code for algorithm 2.
Algorithm 2: Setting up atomsWeight |
---|
Resize atomsDistance to total number of atoms being rendered; for Every atom to be rendered do if This atom is in the origial grid then atomsDistancethisAtom = 1.0 else atomsDistancethisAtom = distance to the closes point of the original cell + 1.0 end if end for Resize atomsWeight to total number of atoms being rendered; for Every atom to be rendered do if This atom is in the origial grid then atomsWeightthisAtom = 1.0 else atomsWeightthisAtom= 1.0 / atomsDistancethisAtom end if end for |
Table 3
TVS2 code for algorithm 3.
Algorithm 3: Draw atoms and bonds with spatial perturbation |
---|
XYZ atomPosition = atomPositionthisAtom; XYZ perturbation = perturbationstimeStep; for Every atom in the original grid do glTranslatef(+ atomPosition + perturbation * atomsWeightthisAtom); drawAtom(); glTranslatef(-atomPosition-perturbation * atomsWeightthisAtom); end for XYZ perturbationForAtomA = perturbationstimeStep * atomsWeightatomA; XYZ perturbationForAtomB = perturbationstimeStep * atomsWeightatomB; for Every bond in the original grid do aPoint = atomPosition [ bondsbondId.atomA() ] + perturbationForAtomA; bPoint = atomPosition [ bondsbondId.atomB() ] + perturbationForAtomB; if atom Type of [bonds[ui].getX()] is different to atom Type of [bonds[ui].getY()]) then midPoint = (aPoint + bPoint)*0.5; setColorToAtom(bondsbondId.atomA()) drawBond(aPoint,midPoint); setColorToAtom(bondsbondId.atomB()) drawBond(midPoint,bPoint); else setColorToAtom(bondsbondId.atomA()) drawBond(aPoint,bPoint); end if end for |
Table 4
TVS2 code for algorithm 4.
Algorithm 4: Draw atoms and bonds with spatial perturbation and temporal distortion |
---|
XYZ atomPosition = atomPositionthisAtom; Int perturbationIndex = timestep - atomsDistancethisAtom; if perturbationIndex is less than 0 then perturbationIndex = 0; end if XYZ perturbation = perturbationstimeStep−perturbationIndex; for Every atom in the original grid do glTranslatef(+ atomPosition + perturbation * atomsWeightthisAtom); drawAtom(); glTranslatef(-atomPosition-perturbation * atomsWeightthisAtom); end for int perturbationIndexForAtomA = timestep - atomsDistanceatomA; XYZ perturbationForAtomA = perturbationsperturbationIndexForAtomA * atomsWeightatomA; int perturbationIndexForAtomB = timestep - atomsDistanceatomB; XYZ perturbationForAtomB = perturbationsperturbationIndexForAtomB * atomsWeightatomB; for Every bond in the original grid do aPoint = atomPosition [ bondsbondId.atomA() ] + perturbationForAtomA; bPoint = atomPosition [ bondsbondId.atomB() ] + perturbationForAtomB; if atom Type of [bonds[ui].getX()] is different to atom Type of [bonds[ui].getY()]) then midPoint = (aPoint + bPoint)*0.5; setColorToAtom(bondsbondId.atomA()) drawBond(aPoint,midPoint); setColorToAtom(bondsbondId.atomB()) drawBond(midPoint,bPoint); else setColorToAtom(bondsbondId.atomA()) drawBond(aPoint,bPoint); end if end for |
G. Propagation along multiple cells –Time lag
A time lag was also implemented as a function of distance from the original cell. The atomsDistance array was reused to determine how many timesteps to step back as the atoms got further away from the initial cell (Table 4).
H. Video conpositing
The atomic crystal lattice animation was then composited with the original audio source and the FRAILMatics logo added using Adobe Premier Pro (R2022; Adobe Inc., San Jose, CA, United States). The final video was then rendered in MPEG-4 format at 1562 x 932 (1.0) pixel resolution, 60 fps, progressive field order with 2-pass variable bitrate (VBR: target bitrate: 27 Mbps, maximum bitrate: 30 Mbps), audio: AAC 320 kbps, 48kHz, stereo.