(2023-04-18) ShellBeat is possible too, but... ---------------------------------------------- ...with many caveats. At least when we want to make it work in Busybox-like environments. Yes, we all know that even the ash version in BB supports bitwise operators and we can wrap formulas almost as they are defined in C into something like $((255&($OUR_FORMULA))) in a loop and be happy. However, as much as you're tempted to use native loops to create the stream and and printfs to convert the decimal output of the formula into hexadecimal and then output as raw bytes, please don't. Printfs will work but they are _extremely_ slow, especially in BB where printf isn't a shell builtin. If you need to generate an endless byte stream at the 8000 Hz rate and higher, you have to take another way. Luckily, BB (and every other POSIX environment) contains all the commands we need to do this. There will be another caveat but we'll get to it later. So, let's split our task of expression-based sound generation into several parts: 1. Generating an endless integer sequence starting from 0. 2. Calculating the formula. 3. Converting the formula result into a raw byte. 4. Passing the byte into the playback program. Step 4 is essentially the same as used in AwkBeat, so I won't repeat myself. Here, let's assume we have SoX installed and exported the variables SAMPLERATE=8000 and PLAYCMD="play -q -tu8 -r${SAMPLERATE} -", so we don't have to write this part fully in the further examples. Now, let's start from the beginning. To generate an endless integer sequence, we could use the seq command as I had shown in my initial post about ByteBeat, but the issue is it's not endless and we have to specify the maximum integer, which depends on the target platform and compile time configuration of the shell itself. Alternatively, we can find something that generates endless lines and something that numbers them. And the first thought would be to use something like yes '' | cat -n, but this also poses several problems: cat doesn't allow us to select which line number to start from and smaller line numbers are preformatted. Luckily, BB/POSIX environments also offer us a special command to number lines, nl, which we can parameterize to not have preformatting and to start from 0. So, the endless integer sequence generation part ultimately looks like this: yes '' | nl -ba -v0 -w0 Now that we have a stream of integers from 0 to whatever, what's next? Next, we need to calculate the formula. Let's assume we have it saved into the ${FORMULA} variable and it's a string with a valid shell-compatible math expression that takes t as the single parameter. And how do we substitute the parameter in the string that came from the standard input? This is what the xargs command is for (that also allows us to supply the parameter name to substitute). A naive approach would be to directly write something like this: yes '' | nl -ba -v0 -w0 | xargs -It echo "$((255&(${FORMULA})))" But, if you run this, you'll find out that all the lines return a single value (the result of the formula being calculated against 0). That's because the shell we're running this in does the substitution of the nonexistent variable t (cast to 0 in math shell expressions) even before it gets to xargs. That's why we need to escape the dollar sign. But then, we'll just get a bunch of shell expressions printed. In order to evaluate them, we need to turn them into commands that we can pass to the actual shell afterwards. So, the final variant of this stage will look like this: yes '' | nl -ba -v0 -w0 | xargs -It echo "echo \$((255&(${FORMULA})))" | sh So, we've got our byte stream calculated and printed in decimal, which is not quite what we want, right? We can use a printf command instead of the inner echo but, as I already said, it will be extremely slow. Instead, let's use another commonly available command that will do the job for us, dc: yes '' | nl -ba -v0 -w0 | xargs -It echo "echo \$((255&(${FORMULA})))P" | sh | dc Here, we append the P instruction to every number, which tells dc to output the value from the stack top as a string if it's a string, or as raw bytes if it isn't. Our case is the latter. Finally, we just pipe this raw byte stream into our player, and this is the ready "ShellBeat" one-liner: yes '' | nl -ba -v0 -w0 | xargs -It echo "echo \$((255&(${FORMULA})))P" | sh | dc | $PLAYCMD > /dev/null But here is another caveat: the P command is non-standard for dc. Yes, it's present in GNU dc and properly configured Busybox builds, but there are some systems where it's just not there. In this case, we have to adjust our one-liner to make dc output our numbers in hexadecimal and also use sed to left-align the output to 2 digits, after which it's passed to our old friend xxd: yes '' | nl -ba -v0 -w0 | xargs -It echo "echo 16o\$((255&(${FORMULA})))p" | sh | dc | sed 's/\<[0-9A-F]\>/0&/' | xxd -r -p | $PLAYCMD > /dev/null Or, if you consider writing "16o" every time an overhead, you can use this version: (echo '16o'; yes '' | nl -ba -v0 -w0 | xargs -It echo "echo \$((255&(${FORMULA})))p" | sh) | dc | sed 's/\<[0-9A-F]\>/0&/' | xxd -r -p | $PLAYCMD > /dev/null Regardless of which of the three one-liners suits you best, any of them is much faster than using printfs to generate the output. But still, there are too many pipes and substitutions. Are they really necessary? Is there any way to just evaluate our formula directly in the current shell and then render its output with whatever tool we choose? Well, check this out: (t=0; while true; do printf '%02X\n' "$((255&(${FORMULA})))"; t=$((t+1)); done) | xxd -r -p | $PLAYCMD > /dev/null Wait, what? Didn't I just say that printfs are slow? Well, yes, they are terribly slow when generating bytestreams directly. But here, we are generating the source for xxd, and we're doing it line by line. In this case, printf works just as fast as echo. And xxd then reconstructs the binary stream as quickly as it can. This is what I have actually used in my shellbeat.sh script published on the main hoi.st page along with AwkBeat. Moral of the story: when there is more than one way to do it, keep exploring every possible option until you find the simplest one. --- Luxferre ---