8f4e is a stack-oriented programming language with a semi-visual interface that I originally designed to perform generative music on algorave events. It's meant to be an efficient, but at the same time portable tool for real-time audio signal generation and processing. One of its most unique features is its representation of pointers using interconnected wires.
- The syntax and commands of 8f4e were inspired by assembly languages, but instead of the typical cryptic mnemonics like
cndjmp
, 8f4e uses more descriptive operation names such asbranchIfTrue
. - The code is organized into modules, each containing variable declarations and a sequence of commands.
- It supports real-time manual modification of variable values while the program is running, without needing recompilation.
- Variables declared sequentially in the code are allocated in adjacent memory locations. For example, if
int foo
is at byte 256, then the nextint bar
will be at byte 260 (assuming a 4-byte word size). - Arrays are also stored in contiguous blocks, enabling straightforward and efficient iteration.
- All variables in 8f4e are inherently public, with no option to modify visibility. Also, it's not memory safe, pointers can point to anything within the memory space of the program, but the wires help developers to find where their pointers are pointing.
- Runtime memory allocation is not supported in 8f4e; developers must pre-plan their software's memory needs during the coding process.
- The language utilizes C-style pointer notations and introduces a new notation:
array&
that retrieves the address of the last word in an array. - The execution order of the code modules is determined by their dependencies. If a module's output is needed as input for others, it is executed first. This creates a sequential flow, where each module executes only after receiving the necessary data from a preceding module's output.
- For performance reasons, 8f4e does not include transcendental functions in its standard library. Instead, it encourages the use of polynomial approximations for these functions.
There are currently two browser-based runtimes, both integrated into the development editor. These runtimes are designed to handle specific types of real-time data processing, such as MIDI events and audio signals.
-
WebWorkerMIDIRuntime: This runtime is for sending and receiving MIDI events, such as note on/off and control change messages. It is built on the WebWorker API and uses the borwser's WebAssembly runtime. Limitations:
- The sample rate is capped at 50Hz, and it requires permission from the user to access MIDI resources within the browser.
- This runtime is not supported in Safari and iOS mobile browsers.
-
AudioWorkletRuntime: This runtime handles audio signal processing, ideal for building synthesizers or doing real-time audio analysis. It supports standard sample rates like: 22,050 Hz and 44,100 Hz. It’s built on the AudioWorklet API and uses the browser’s WebAssembly runtime. Limitations:
- Because of browser security policies, you’ll need a user action (like a click or tap) to start audio playback or processing.
- The input audio buffer is fine, but don’t expect studio-grade quality. As of 2024, it’s really meant for things like voice memos or audio messages.
- If you’re using both input and output at the same time, the audio quality can take a noticeable hit in Chrome and Firefox. If you want to monitor the output while recording, Safari does a better job handling it (for now).
- DaisyARMCortexM7: Coming soon...
- LinuxALSA: Coming soon...
- MacOSCoreAudio: Work in progress...
- To add an init block to the modules, for code that only has to be executed once at the initialisation stage.
- To write a runtime for microcontrollers.
- Unify the two browser runtimes for MIDI and Audio I/O.
- To write a JavaScript-based runtime for debugging the stack.
- To write a headless runtime in C++ for the ARM architecture using ALSA for audio playback.
- Add collaborative editing features to the editor.
- Optimize rendering in the editor by offloading certain tasks from JavaScript to shaders.
- The compilation is already fast, but I want to re-write the compiler in Rust to make it even faster.
- The sample rate is capped at 50Hz in the
WebWorkerMIDIRuntime
because, as of now, web browsers lack a precise task scheduling API. Based on my measurements on an M1 MacBook running Chrome, 50Hz provided the least-noticeable time divergences. If you're using an older processor architecture and experience a lot of swinging in your MIDI projects, you may need to reduce the sample rate to improve precision. - The
WebWorkerMIDIRuntime
is not supported on Safari or iOS mobile browsers due to Apple’s refusal to implement the Web MIDI API. - Due to security and privacy concerns, browsers enforce strict controls over audio and MIDI resource access, therefore explicit user interaction or permission is required to use the
WebWorkerMIDIRuntime
andAudioWorkletRuntime
browser runtimes.
- Arithmetic instructions
- Bitwise instructions
- Comparison
- Control flow instructions
- Conversion
- Memory instructions
- Other
The "add" instruction operates on two numbers of the same type that are retrieved from the stack. It performs addition on these numbers and then stores the result back onto the stack.
push 1 # stack: [ 1 ]
push 2 # stack: [ 1, 2 ]
add # stack: [ 3 ]
push 0.5 # stack: [ 3, 0.5 ]
push 0.7 # stack: [ 3, 0.5, 0.7 ]
add # stack: [ 3, 1.2 ]
The "div" instruction retrieves two numbers of the same type from the stack and divides the first number by the second. The resulting quotient is then stored back onto the stack.
The "mul" instruction retrieves two numbers of the same type from the stack, multiplies them together, and then stores the result back onto the stack.
push 1 # stack: [ 2 ]
push 2 # stack: [ 2, 2 ]
mul # stack: [ 4 ]
push 0.5 # stack: [ 4, 0.5 ]
push 0.7 # stack: [ 4, 0.5, 0.7 ]
add # stack: [ 4, 0.35 ]
The "remainder" instruction retrieves two integer operands from the stack, divides the first operand by the second operand, and then computes the remainder of this division. It then stores the remainder back onto the stack.
The "sub" instruction operates on two numbers of the same type that are retrieved from the stack. It subtracts the second operand from the first operand, and then stores the result back onto the stack.
push 1 # stack: [ 2 ]
push 2 # stack: [ 2, 3 ]
sub # stack: [ -1 ]
push 0.5 # stack: [ -1, 0.5 ]
push 0.7 # stack: [ -1, 0.5, 0.7 ]
add # stack: [ -1, -0.2 ]
The "and" instruction retrieves two integers from the stack and performs a bitwise AND operation on them. Specifically, each bit of the resulting value is computed by performing a logical AND between the corresponding bits of the two operands. The resulting value is then stored back onto the stack.
push 0b00001 # stack: [ 0b00001 ]
push 0b00100 # stack: [ 0b00001, 0b00100 ]
and # stack: [ 0b00000 ]
clearStack
push 0b00001 # stack [ 0b00001 ]
push 0b00001 # stack [ 0b00001, 0b00001 ]
and # stack [ 0b00001 ]
The "or" instruction retrieves two integers from the stack and performs a bitwise OR operation on them. Specifically, each bit of the resulting value is computed by performing a logical OR between the corresponding bits of the two operands. The resulting value is then stored back onto the stack.
push 0b00001 # stack: [ 0b00001 ]
push 0b00100 # stack: [ 0b00001, 0b00100 ]
and # stack: [ 0b00101 ]
clearStack
push 0b00001 # stack [ 0b00001 ]
push 0b00001 # stack [ 0b00001, 0b00001 ]
and # stack [ 0b00001 ]
The "shiftRight" instruction retrieves two integer operands from the stack. It shifts the bits of the first operand to the right by the number of positions specified by the second operand, and then stores the resulting value back onto the stack. This instruction is typically used to perform bit shifting operations in a program, such as dividing an integer by a power of 2, or extracting specific bits from an integer. Note that if the second operand is greater than or equal to the number of bits in the first operand, the result will be 0.
The "shiftRightUnsigned" instruction retrieves two integer operands from the stack. It shifts the bits of the first operand to the right by the number of positions specified by the second operand, filling the leftmost bits with zeros, and then stores the resulting value back onto the stack. This instruction is similar to the "shiftRight" instruction, but treats the first operand as an unsigned integer. This means that no sign extension occurs during the shift, and the leftmost bits are always filled with zeros, even if the original leftmost bit was
The "xor" instruction retrieves two integers from the stack and performs a bitwise XOR (exclusive OR) operation on them. Specifically, each bit of the resulting value is computed by performing a logical XOR between the corresponding bits of the two operands. The resulting value is then stored back onto the stack.
The equalToZero instruction retrieves a value from the stack, verifies if it equals zero, and then pushes a 1 onto the stack if it is true, or 0 if it is false.
The greaterOrEqual instruction obtains two values from the stack, checks if the first value is greater than or equal to the second value, and then pushes 1 onto the stack if it is true, or 0 if it is false.
The greaterOrEqualUnsigned instruction retrieves two unsigned values from the stack, checks if the first value is greater than or equal to the second value without considering their signs, and then pushes 1 onto the stack if it is true, or 0 if it is false.
The greaterThan instruction retrieves two values from the stack, checks if the first value is strictly greater than the second value, and then pushes 1 onto the stack if it is true, or 0 if it is false.
The lessOrEqual instruction takes two values from the stack, checks if the first value is less than or equal to the second value, and then pushes 1 onto the stack if it is true, or 0 if it is false.
The lessThan instruction retrieves two values from the stack, checks if the first value is strictly less than the second value, and then pushes 1 onto the stack if it is true, or 0 if it is false.
The castToFloat instruction takes an integer from the stack, converts it to a floating-point number, and then places the resulting value back onto the stack.
The castToInt instruction takes a value from the stack, converts it to an integer, and then places the resulting integer back onto the stack.
The natural environment for 8f4e are virtual machines like WebAssembly, which often use a stack machine architecture. Stack machines have fewer hardware dependencies than register-based machines, making them easier to port to different platforms. A programming language that is designed with a stack-oriented approach can perform operations on the stack in a manner that is both efficient and natural, with instructions that can be easily mapped to those of the stack machine.
Visual programming languages use graphical elements, such as icons, symbols, and flowcharts, to represent programming concepts and logic, instead of traditional text-based code. 8f4e combines these graphical elements with text-based code.
Okay, this one is actually asked a lot. I wrote a script to hunt down the shortest available .com domains, and 8f4e was one that caught my eye because it only included digits and letters from the hexadecimal system. When I registered it, I still had no idea what I would use it for. The project came later, and I picked this domain name for the website. Later, I found out that it's the UTF-8 code for the Chinese character "轎," which means "litter" (a human-powered vehicle, not cat litter or rubbish).