Pi Alternatives

In November 2025 I did some panic purchasing. I upgraded one desktop to 160 gigs of memory, another to 64 gigs. I Upgraded my laptop to 70 gigs of memory. And I purchased 52 terabytes of storage of various types. At the time, some friends thought I was being a bit ridiculous. In retrospect, I feel like I didn’t panic enough. A walk through Microcenter this weekend showed me that the prices for the items that I got have gone up significantly. Memory prices have affected a lot of things. Generally, we just see the price of an item go up. Or, if you look closely enough, you may find the capabilities of something diminished. On occasions, I see memory prices called out as the reason a price has changed.

The Raspberry Pi has been significantly affected by memory prices. At the moment I have idle units available, but I was looking at what my options are should I want to get another unit without breaking the bank. These are the options that I found.

Orange Pi Series

The Orange Pi series of boards are also affected by the increase in memory prices. But they tend to be more affordable than the Pi. You get more memory and computational capability for what you pay, including a built in NPU for AI applications. The tradeoff is that it isn’t 100% compatible with Pi applications (with camera applications being identified as a category that sees conflicts) and the support community is not as large as the community for the Raspberry Pi.

The Orange Pi 5 Plus also has an HDMI input for video capture and processing. This could be used for a different type of camera interface. The ORange Pi SBCs typically do not have integrated WiFi. But WiFi can be added with a either a USB accessor and in some cases a B-Key M.2. The Orange Pi boards do not have built in storage for a file system. You’ll need to use either an SD card or an M.2 drive.

ModelWirelessRAMPrice
Orange Pi 5 4-GigNo4 Gig126 USD
Orange Pi 5 8-GigNo8 Gig184 USD
Orange Pi 5 16-gigNo16 Gig335 USD
Orange Pi 6 Plus 16-GigNo16 Gig366 USD

Libre Sweet Potato

The Libre Sweet Potato (AML-S905X-CC-V2) is available for just 60 USD. For 10 USD more you can add PoW to the unity. These units have 2 gigs of ram have an SD card slot for storage. The supported operating systems include Fedora 42, Debian 13, Ubuntu 24.04 LTS, and CoreELEC.

Banana Pi BPi M2 Zero

This unit is comparable to the Raspberry Pi Zero W. It is able to run Raspbian, Ubuntu, Debian, and Android. It only cost 22 USD. The reason that someone might want to consider this isn’t so much the price, but because of the demand for the Pi Zero outpacing the supply. Efforts are being made to address the shortage. Until then, this board might be a substitute. At the time of this writing, it cost 22 USD.

Tinker Board 3

The Tinker Board 3S uses the Rock Chip RK3566. The Tinker Board 3 has no inbuilt storage, while the 3S has a 16 gig emcc. Both versions also have an SD card slot. They are available with 2 gigs or 4 gigs of RAM.

ModelStorageRAMPrice
Tinker Board 3None290 USD
Tinker Board 3None4104 USD
Tinker Board 3S16-Gig2106 USD
Tinker Board 3S16-Gig4120 USD

Consider the Pi 3B

The Pi 3 models also run the 64-bit versions of the Pi OS. At the time of this writing, August 2026, The Pi 3B can run Pi OS Trixie (Released June 2026) available in 32-bit and 64-bit versions. You can generally get a Pi 3B on Amazon for about 55 USD or less for a 1 gig unit.

Ebay

I’ve found sellers that have Pi units at prices that are well below what some other retailers are charging for them. The units I found ranged from new, still in box to lightly used units. Most of the units I found were Pi4 models. It makes sense that these are more plentiful since they were available on the market for a longer run of years than the Pi 5.

Are your Requirements as High as You are Aiming?

You may also want to consider whether what your requirements actually are. When prices were cheaper, it was easy to just get something that may have had more memory than needed. For 70-85 USD, which is a bit more than the Pi 3B, you can move up to a Pi 4B with 1 gigs of RAM. If you want to get 2 gigs of ram instead, they can be had for about 100 USD (Pi 4B with 2 Gig RAM).

The Pi CM4 units vary a lot more since you have the option to have wireless or not, to have in built storage or not, and varied amounts of RAM. Wi-Fi has low impact on the price.

ModelStorageWirelessRAMPrice
CM4001000NoneNo1 Gig75 USD
CM4101000NoneYes1 Gig81 USD
CM41010088 GigYes1 Gig110 USD
CM4102000NoneYes2 Gig104 USD
CM4104000NoneYes4 Gig150 USD
CM410803232 GigYes32 Gig284 USD

The Pi CM 5 prices have gone up significantly. I compared a purchase I made in March to prices today. They cost almost twice the amount that I paid. I would suggest leaving the CM5 alone unless you really need it.

ModelStorageWifiRAMPrice
CM5102000NoneYes2 Gig119 USD
CM500201616 GigNo2 Gig142 USD
CM510401616 GigYes4 Gig198 USD
CM510801616 GigYes8 Gig288 USD
CM511603216 GigYes32 Gig455 USD

Posts may contain products with affiliate links. When you make purchases using these links, we receive a small commission at no extra cost to you. Thank you for your support.

Mastodon: @j2inet@masto.ai
Instagram: @j2inet
Facebook: @j2inet
YouTube: @j2inet
Telegram: j2inet
Bluesky: @j2i.net

“Everything is High Priority”

In recent times, I’ve personally and vicariously encountered a reoccurring situation concerning competing needs and tasks. It can be summarized to lack of prioritization. Fundamentally, I think we all understand prioritization. If we need to address problems in a house to prepare for a visit, and the house has a door that needs to be repaired, an electrical socket that shorts out, and a fire on the stove it is pretty easy to decide what problem will be addressed first. The consequences of some problems increase with time. The fire will get more unwieldly, a bill will incur fees, the door may allow unwanted water or insects to enter a structure. These consequences differ in severity and growth speed.

The problem I and others encounter is that the use of the word “priority” is being used not to express some ranking of importance, but to express something that they wish to have done. There is a wish for all things to be done coupled with the lack of resources (especially time or manpower resources) to do it and meet deadlines. This creates tensions and problems. There may be times when conditions on a project result in this problem as a temporary and short-lived problem. But there are also groups for which these conditions are their mode of operation. Trends in differences in how I see these problems dealt with correlate to age and work experience among the people through which I’ve observed them. For the younger and less experienced workers, this creates unmanageable conditions is misery as they find their life being consumed by these tasks taking up more hours, after-hours, and weekends. The “solution” to these problems have been to find another job (if there is time to find one). That’s not an entirely unreasonable solution; sometimes these conditions are a reflection of other chaos within an organization.

Older or more experienced employees may do the same, or they may give reasonable pushback. Someone I spoke with was out of town to perform a deployment for a marketing event at some sports event. A lot of time was spent for setup, walking the client through the parts of the event and fixtures, interacting with media, and keeping hired staff in line. While this was going on people in the office, though aware of the event, were sending request for work to be done with deadlines of the next day. The person asked how they should prioritize these requests and got back the “everything is a priority.” The worker let them know “not everything will be done.” The reasonable refusal to satisfy all requests was a forcing function that resulted in the requests getting rankings.

I most recently encountered a similar situation when I was preparing to leave town for a project. For a previous project, a decision was made to change the requirements (something that happened a lot on that project). On the Sunday that I was going to use to finish packing and get things in order at home I found myself on an unscheduled team meeting. I hadn’t planned to be on a 9am meeting on a Sunday, or for that meeting to last 5 hours. At the end of that 5 hours there were changes that I needed to implement and other requests. There was a request that when I get done with the changes, that we have another meeting so that we could live-test them and make alterations during a meeting. I gave a polite refusal and plainly stated that the meeting had already caused disturbances to personal activities and activities for my other project. I told them I wouldn’t be participating in their live testing and let them know that their full list of requests will not be addressed today or this week. This, once again, gave the necessary motivation to think about importance rankings.

My personal and vicarious experiences might not be representative of wider trends. But they do form my views. I am getting the impression that “priority” is being used more as corporate jargon. That it is being used in a way that often differs from the conventional usage of the word is a bit disappointing. But it appears that helping someone understand that all desires being satisfied is not an option (and being steadfast in doing so) is a solution for motivating others to participate in ranking priorities.


Posts may contain products with affiliate links. When you make purchases using these links, we receive a small commission at no extra cost to you. Thank you for your support.

Bluesky:ย @j2i.net
YouTube:ย @j2inet
Mastodon:ย @j2inet@masto.ai
Instagram:ย @j2inet
Facebook:ย @j2inet
Telegram:ย j2inet

Dynamic Sound on Windows and Linux (Pi)::Part 1

Some time ago I made a post on dynamically rendering sound in the browser. In furtherance of a different project, today I am writing on dynamically rendering sound on Windows and Linux. I was specifically targeting the Raspberry Pi. But the Pi approach also works on other forms of Linux. For my project, I don’t want to do a complete rewrite for Windows and Linux. I want to be able to share code between them. For this approach, there are features of the Sound APIs on each operating system that I will not be taking advantage of. Doing so binds code more strongly to that operating system and works against my goal of keeping the code generic.

For this post, I want to play a Sine wave at a frequency of 440 Hz (Middle-A, if you are familiar with music). The code I wrote to do this falls into two categories; OS agnostic code, and OS specific code. Some design decisions of the OS agnostic code show a future consideration. I want to be able to modify parameters of generated sounds through files that can be modified post-compilation. You will see the use of dictionaries to handle sound parameters instead of fields. It will be easier to bridge the dictionaries to text files.

The code can be found here.

Playing Dynamic Sound on Windows

There are a variety of Sound APIs for playing sound on Windows. I decided on XAudio2. The XAudio2 API is made with video games in mind. It provides a low latency audio interface to which we can submit sound buffers. To invoke methods in the API, we need to get an object that implements the IXAudio2 interface. This object will be used to create other XAudio objects. The XAudio object will be used to create a “voice.” A voice is simply something that produces sound.

ComPtr<IXAudio2> pxAudio{};
IXAudio2MasteringVoice* pxMasteringVoice{};
HRESULT result;
result = XAudio2Create(&pxAudio, 0, XAUDIO2_DEFAULT_PROCESSOR);
result = pxAudio->CreateMasteringVoice(&pxMasteringVoice);

I must declare information on the format of the data that I will play. I will have the data sampled at 44.1 kHz to play single channel (mono) 16-bit audio. With this formatting information, I can create an IXAudio2SourceVoice object. This object will play whatever samples are fed to it in the order that they are submitted with no gap between them.

const int SAMPLE_RATE = 44100; // 44.1 kHz sample rate 
WAVEFORMATEX wfx = {};
wfx.wFormatTag = WAVE_FORMAT_PCM;
wfx.nChannels = 1;
wfx.nSamplesPerSec = SAMPLE_RATE;
wfx.wBitsPerSample = 16;
wfx.nBlockAlign = wfx.nChannels * wfx.wBitsPerSample / 8;
wfx.nAvgBytesPerSec = wfx.nSamplesPerSec * wfx.nBlockAlign;

result = pxAudio->CreateSourceVoice(&pxSourceVoice, &wfx, 0, XAUDIO2_DEFAULT_FREQ_RATIO, nullptr, nullptr, nullptr);

I’ll set aside 4 buffers to hold the audio data.

const int BUFFER_COUNT = 4;
const size_t BUFFER_SIZE = 44100; // 1 second of audio at 44.1kHz
std::vector<short> audioBuffers[BUFFER_COUNT];

To fill the buffer, I ask my sound generation function (I’ll discuss this shortly) to return the sample for each time offset. 44,100 samples are needed for one second of audio. I’ll make 4 audio buffers, populate them, and submit them to the voice object.

float currentTime = 0.0f;
float deltaTime = 1.0f / SAMPLE_RATE;
int currentBufferIndex = 0;

for (int bufferIndex = 0; bufferIndex < BUFFER_COUNT; ++bufferIndex) {
	audioBuffers[bufferIndex].resize(BUFFER_SIZE);
	for (size_t sampleIndex = 0; sampleIndex < BUFFER_SIZE; ++sampleIndex) {
		float sampleValue = voice->getSample(currentTime); // Get the sample from the voice
		audioBuffers[bufferIndex][sampleIndex] = static_cast<short>(sampleValue * 32767); // Convert to 16-bit PCM
		currentTime += deltaTime;
	}
}

Now that the buffers are populated, I can submit them to the XAudio2 object. If I wanted to keep playing sounds continuously, I could register a callback to know when XAudio2 has completed playing a buffer so that I can populate it with the next segment of sound and resubmit it. For now, I won’t concern myself with this and will just let the 4 samples play and then terminate the application.

for (auto i = 0; i < BUFFER_COUNT; ++i)
{
    XAUDIO2_BUFFER buf = {};
    buf.AudioBytes = BUFFER_SIZE * sizeof(short);
    buf.pAudioData = (BYTE*)audioBuffers[i].data();
    result = pxSourceVoice->SubmitSourceBuffer(&buf);
}

result = pxSourceVoice->Start();

XAudio2 calls are non-blocking. To prevent the code from running to its end and exiting before any sound is played, I put the main thread to sleep for 4 seconds. After the audio plays, I free the resources that were being used to play the audio and terminate.

Sleep(4000);

pxSourceVoice->Stop();
pxSourceVoice->DestroyVoice();
pxMasteringVoice->DestroyVoice();
CoUninitialize();

Playing Dynamic Sounds on Linux

On Linux, I use the ALSA library (Advanced Linux Sound Architecture). Before coding, there are components that may need to be installed on your system. I installed the following.

sudo apt install alsa-utils libasound2-plugins libasound2
sudo apt install libasound2-dev

Without doing that, you may run into compilation errors from headers and libraries not being found. In the code, you’ll want to open up the default PCM device for playback. I did encounter a problem on one of my Pis that I have yet to resolve. Though it had PCM playback devices, none of them were defaults. Searching for this problem, I found that some say that you must configure a device to be the default to get around this problem. I just used a different PI.

int pcm;
if ((pcm = snd_pcm_open(&pcm_handle, PCM_DEVICE, SND_PCM_STREAM_PLAYBACK, 0)) < 0) {
   std::cerr << "ERROR: Can't open \"" << PCM_DEVICE << "\" PCM device. " << snd_strerror(pcm) << "\n";
   return 1;
}

After the device is successfully opened, we need to get a hardware parameters object and populate it.

snd_pcm_hw_params_t *params;
unsigned int sample_rate = 44100;
int channels = 2;
snd_pcm_uframes_t frames = 32;     // Frames per period


snd_pcm_hw_params_alloca(&params);
//If audio has more than one channel, such as stereo audio, the channels will be interleaved
snd_pcm_hw_params_set_access(pcm_handle, params, SND_PCM_ACCESS_RW_INTERLEAVED);
//Samples will be 16-bit
snd_pcm_hw_params_set_format(pcm_handle, params, SND_PCM_FORMAT_S16_LE);
//Set number of channels and sample rate
snd_pcm_hw_params_set_channels(pcm_handle, params, channels);
snd_pcm_hw_params_set_rate_near(pcm_handle, params, &sample_rate, nullptr);
//Set how often hardware notifies of progress. Note that the request might not be conformed to. It is hardware dependent.
snd_pcm_hw_params_set_period_size_near(pcm_handle, params, &frames, nullptr);

//Now that the hardware parameters are populated, apply them to the device
if ((pcm = snd_pcm_hw_params(pcm_handle, params)) < 0) {
    std::cerr << "ERROR: Can't set hardware parameters. " << snd_strerror(pcm) << "\n";
    snd_pcm_close(pcm_handle);
    return 1;
}

//Read back the actual frame size and create a buffer accordingly.
snd_pcm_hw_params_get_period_size(params, &frames, nullptr);
int buffer_size = frames * channels * 2; // 2 bytes/sample (S16_LE)
char *buffer = new char[buffer_size];

From here, the code starts to look more similar to the windows code. I keep track of a time offset and request a sample for each time segment. That sample is populated into a buffer. Once the buffers are populated, they are submitted to be played.

    float time_delta = 1.0f / static_cast<float>(sample_rate); // Time increment per sample
    float current_time = 0.0f; // Initialize current time

    for (int i = 0; i < sample_rate * 2; ++i) { // Play for ~2 seconds
        for (int f = 0; f < frames; ++f) {
            short sample = voice->getSample(current_time)[0] * 32767.0f; // Convert float sample to 16-bit PCM
            current_time += time_delta; // Increment time for the next sample
            for (int c = 0; c < channels; ++c) {
                buffer[(f * channels + c) * 2] = sample & 0xFF;
                buffer[(f * channels + c) * 2 + 1] = (sample >> 8) & 0xFF;
            }
        }

        // Write to PCM device
        if ((pcm = snd_pcm_writei(pcm_handle, buffer, frames)) == -EPIPE) {
            snd_pcm_prepare(pcm_handle); // Recover from underrun
        } else if (pcm < 0) {
            std::cerr << "ERROR: Write to PCM device failed. " << snd_strerror(pcm) << "\n";
            break;
        }
    }

Once done playing, we clean up the resources and terminate the program.


delete[] buffer;
snd_pcm_drain(pcm_handle);
snd_pcm_close(pcm_handle);

Generating the Sound Samples

When generating a sound, there may be parameters or modifiers. A common such parameter used in music would be frequency/pitch/note. I’ve defined a base class for anything that makes a noise, simply calling it Voice.

typedef std::wstring StringType; // Define a type alias for std::wstring

class VoiceBase {
    public:
        VoiceBase() = default;
        virtual ~VoiceBase() = default;

        virtual float getSample(float time) = 0; // Pure virtual function to get the sample at a given time
        void setIntParameter(const StringType &name, int value); // Pure virtual function to set an integer parameter
        void setFloatParameter(const StringType &name, float value); // Pure virtual function to set a float parameter
        // Set the volume of the voice (0.0 = silent, 1.0 = max)
        void setVolume(float newVolume);
        // Get the current volume of the voice
        float getVolume() const;
    protected:
           std::map<StringType, int> intParameters; // Map to store integer parameters
        std::map<StringType, float> floatParameters; // Map to store float parameters
    private:
};

The most important method here is VoiceBase::getSample(float time); Given some time offset, it returns what the sound sample will be at that offset. This base class is abstract, the getSample() function virtual and undefined, and this class itself is incapable of returning samples as defined. I’ve made another class that inherites from this foundation class named SineWaveVoice. It plays a sine wave at some specified frequency.

class SineWaveVoice : public VoiceBase {
    public:
        SineWaveVoice(float frequency=440, float amplitude=1)
            : VoiceBase() {
            setFloatParameter(L"frequency", frequency);
            setFloatParameter(L"amplitude", amplitude);
        }

        float getSample(float time) override {
            float theta = 2.0f * std::numbers::pi * getFrequency() * time;
            float sample = getVolume() * (sin(theta) + 0.5 *sin(theta * 2.0f))/1.5f;
            return sample; // Return the sample as a vector
        }
        inline void setFrequency(float newFrequency) {
            setFloatParameter(L"frequency", newFrequency);
        }

        inline float getFrequency() const {
            auto it = floatParameters.find(L"frequency");
            if (it != floatParameters.end()) {
                return it->second;
            }
            return 440.0f; // Return the default frequency if not set
        }
    private:

};

The getSample() method it does a sine calculation, modifying the amplitude according to the volume setting, and returns the value.

float getSample(float time) override {
    float theta = 2.0f * std::numbers::pi * getFrequency() * time;
    float sample = getVolume() * sin(theta);
    return sample; // Return the sample as a vector
}

Compiling the Code

For Windows, there is a Visual Studio 2026 solution (*.slnx). Open that solution file and press [F5] to see the code compile and run. On Linux, I made a script named build-executable.sh which builds the execute from the code and outputs it.

What’s Next

Having played a sine wave, my next goal is to produce sounds that are recognized as music. We will want to be able to generate simultaneous sounds so that the music can have polyphony. This will get the code up to par with the code I wrote in JavaScript.


Posts may contain products with affiliate links. When you make purchases using these links, we receive a small commission at no extra cost to you. Thank you for your support.

Mastodon: @j2inet@masto.ai
Instagram: @j2inet
Facebook: @j2inet
YouTube: @j2inet
Telegram: j2inet
Twitter: @j2inet