Sonifying the Tides with J
Prior to my presentations at the DSP Online Conference and the inaugural Signal Processing Summit I ported my C code for sonifying the tides to the J language. I like how terse it is compared to the C version.
NB. tides.ijs, sonify the tides using harmonic constituent data from NB. NOAA. NB. NB. Copyright © 2025 Remington Furman NB. NB. SPDX-License-Identifier: MIT NB. NB. Permission is hereby granted, free of charge, to any person NB. obtaining a copy of the software demonstrated in code cells (the NB. "Software"), to deal in the Software without restriction, including NB. without limitation the rights to use, copy, modify, merge, publish, NB. distribute, sublicense, and/or sell copies of the Software, and to NB. permit persons to whom the Software is furnished to do so, subject NB. to the following conditions: NB. NB. The above copyright notice and this permission notice shall be NB. included in all copies or substantial portions of the Software. NB. NB. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, NB. EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF NB. MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NB. NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS NB. BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN NB. ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN NB. CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE NB. SOFTWARE. NB. Exit on error: 9!:29]1[9!:27'2!:55]1' NB. Use ASCII boxes: boxdraw_j_ 1 NB. wav.ijs customizes some code from the media/wav addon. load './wav.ijs' NB. Save as a .wav file. save_wav=: dyad define (SAMPLE_RATE wavmake x) 1!:2 <y ) NB. Adapted from https://code.jsoftware.com/wiki/User:Devon_McCormick/Palettes round_even=: monad define <.y+0.5*(0~:2|<.y)+.0.5~:1|y ) NB. Audio setup SAMPLE_RATE=: 44100 NB. Hz SAMPLE_SIZE=: 16 NB. bits NB. Scaling factors NB. TODO: Determine max amplitude encountered at any station. GAIN=: 0.3 NB. Allow for headroom. MAX_PCM_AMP=: (2^(SAMPLE_SIZE-1))*GAIN FT_TO_AMP=: MAX_PCM_AMP % 13.0 DPH_TO_RPS=: 2p1%(60*60*360.0) NB. (degrees/hour) to (rad/second) FREQ_SCALE=: 60*60*10000.0 NB. times realtime NB. Synthesize a single harmonic constituent. NB. x: number of output samples NB. y: harmonic constituents (amplitude, phase, speed) synth_harcon=: dyad define "0 1 amp=. 0{y phase=. 360 %~ 2p1 * 1{y ang_vel=. FREQ_SCALE * DPH_TO_RPS*(2{y) % SAMPLE_RATE phases=. phase + 2p1|ang_vel*(i. x) ]samps=. amp * 1 o. phases ) NB. Synthesize all the harmonic constituents at a tide station and NB. save the waveform as a .wav audio file. NB. x: number of output samples NB. y: boxed station info synth_station=: dyad define id=.".>{.{.{. y harcons=.".>(3 4 5) {"1 y NB. Just the facts, please. feet=.+/ x synth_harcon harcons samps=. round_even FT_TO_AMP*feet echo (":id) samps save_wav './output/', (":id), '.wav' 0 NB. We only care about side-effects, so return a dummy value. ) NB. Read harmonic constituents: load 'tables/csv' dat=: readcsv jpath 'harcon_amp.csv' NB. Group and box each station: stations=: ({."1 dat) </. dat NB. Uncomment to use a short dataset for testing: NB. stations=: 100{.stations main=: monad define DUR_SEC=. 10.0 DUR_SAMP=. round_even SAMPLE_RATE*DUR_SEC if. 0 do. NB. Single threaded DUR_SAMP (synth_station >)"0 stations else. NB. Multi-threaded. NB. Initialize threadpool 0 with N-1 threads. {{0 T.0}}^:] <: {. 8 T. '' ]&.> DUR_SAMP (synth_station >)t.''"0 stations NB. The ]&.> verb waits for every pyx to be populated, but is NB. otherwise a no-op. NB. J9.6 will crash if exit is called and tasks are still running. NB. J9.7 won't crash, but also doesn't wait for unfinished threads. end. ) main'' exit''
The code uses this CSV data file which has all non-zero amplitude harmonic constituents for all 1274 NOAA tide prediction stations (as of 2018):
Some Notes on Learning J
It was relatively easy to make this multi-threaded. A few more examples would have been helpful, but I was able to figure it out.
While I was writing this I wanted to use the 2!:2 foreign
conjunction to pipe a verb's output directly to sox -c 1 -r 44100 -t
s2 - -d to play audio instead of writing raw samples to an
intermediate file then converting to an audio file to play later. But
the documentation for 2!:2 wasn't very clear and didn't provide
usage examples. Worse, I learned this foreign conjunction was removed
in J9.03 and that is not yet reflected in the Vocabulary. It's too
bad, because Unix pipelines are a very powerful way to compose
software and in my opinion one of the great lessons in software
engineering. J's design is largely about composing verbs in ways that
are often more complicated than simple pipelines, but only within J
code. It's unfortunate they can't mix.
I then learned about the media/wav addon, which provides verbs for
saving samples as .wav files. The wavmake verb automatically
adjusts the bitdepth of the file based on the contents of the audio,
but that meant that stations with small tides used a different format
from the rest of the stations. There was also a bug in wavmake that
broke some of those smaller files. So I modified it to always use the
same bitdepth and put that in a separate file.
NB. MIT License NB. NB. Copyright (c) 2006 Oleg Kobchenko NB. Copyright (c) 2018 Jsoftware Inc. NB. Copyright (c) 2025 Remington Furman NB. NB. Permission is hereby granted, free of charge, to any person obtaining a copy NB. of this software and associated documentation files (the "Software"), to deal NB. in the Software without restriction, including without limitation the rights NB. to use, copy, modify, merge, publish, distribute, sublicense, and/or sell NB. copies of the Software, and to permit persons to whom the Software is NB. furnished to do so, subject to the following conditions: NB. NB. The above copyright notice and this permission notice shall be included in all NB. copies or substantial portions of the Software. NB. NB. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR NB. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, NB. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE NB. AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER NB. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, NB. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE NB. SOFTWARE. NB. Copied from media/wav and edited to always use 16 bits per sample. NB. This fixes run-time index errors for stations where y is small. load 'media/wav' BIGEND=: ({.a.)={.1(3!:4)1 '`i2 i4'=: (0 1+2*BIGEND){ (1&ic)`(2&ic)`([: , _2 |.\ 1&ic)`([: , _4 |.\ 2&ic) NB.*wavmake v wav format from samples NB. y samples vector or (2,N)=$y matrix for stereo NB. range is _32768..32767 NB. x sample rate in Hz wavmake=: dyad define bs=. 16 NB. bits per sample ba=. (bs*#$y) <.@% 8 NB. block align br=. x * ba NB. byte rate (bytes/sec) d=. 'data',(,~ i4@#) i2`({&a.)@.(bs=8) ,|:y f=. 'fmt ',(i4 16),(i2 1),(i2 #$y),(i4 x),(i4 br),(i2 ba),i2 bs 'RIFF',(,~ i4@#) 'WAVE',f,d )