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.

tides.ijs:

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):

harcon_amp.csv.gz

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.

wav.ijs:

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
)

© Copyright 2025, Remington Furman

blog@remcycles.net

@remcycles@subdued.social