Since I posted my first version of making music with a power supply, it seems there has been some interest in the topic. Unfortunately, not all of it necessarily that positive. I understand ... art is a little bit like that ... sometimes it's hard to appreciate.
One particular comment on social media did make me chuckle before sighing - "I love chip tune music, but this might be music only a mother could love."
I certainly didn't mean to make such bad music, so I thought I should have another go at it before people declare this whole endeavour a failure. After all, this is "my child", so lets improve things shall we?
From Square Waves to Pulse Waves
For coding simplicity, my first attempt used square waves. What this means is that the time on is equal to the time off - a 50% duty cycle. But because the power supply's QuickArb feature has a 1ms granularity on time-steps, this results in a very limited frequency scale as noted in the last posting:
Dwell (ms) Frequency (Hz) 1 500 2 250 3 166.6666667 4 125 5 100 6 83.33333333 7 71.42857143 8 62.5 9 55.55555556 10 50 11 45.45454545 12 41.66666667 13 38.46153846 14 35.71428571 15 33.33333333 16 31.25 17 29.41176471 18 27.77777778 19 26.31578947 20 25 21 23.80952381 22 22.72727273 23 21.73913043 24 20.83333333 25 20 26 19.23076923 27 18.51851852 28 17.85714286 29 17.24137931 30 16.66666667 31 16.12903226 32 15.625 33 15.15151515 34 14.70588235 35 14.28571429 36 13.88888889 37 13.51351351 38 13.15789474 39 12.82051282 40 12.5
Noting this, we can make music using pulse waves instead. What this means is that in each cycle, I use a single 1ms long positive pulse, followed by a variable length of zero-voltage, to form my sound. This won't sound as smooth as a square wave due to different harmonics arising, but a quick calculation shows that this affords us additional frequency steps in our scale. The net benefit is better pitch accuracy, although playing at a moderate octave is still not possible as the steps in frequency are too wide the further you go up the scale:
Dwell Frequency (Hz) 1 500 2 333.3333333 3 250 4 200 5 166.6666667 6 142.8571429 7 125 8 111.1111111 9 100 10 90.90909091 11 83.33333333 12 76.92307692 13 71.42857143 14 66.66666667 15 62.5 16 58.82352941 17 55.55555556 18 52.63157895 19 50 20 47.61904762 21 45.45454545 22 43.47826087 23 41.66666667 24 40 25 38.46153846 26 37.03703704 27 35.71428571 28 34.48275862 29 33.33333333 30 32.25806452 31 31.25 32 30.3030303 33 29.41176471 34 28.57142857 35 27.77777778 36 27.02702703 37 26.31578947 38 25.64102564 39 25 40 24.3902439
With this is mind, hopefully it will sound better. The code change is rather simple to implement - just fix your first arbitrary point at 1ms of length and compute the duration of the second point as the note period minus 1ms.
Improving the Temporal Resolution
The next thing to notice was the note skipping due to frantic commanding of the NGM202 making it slightly unhappy. I had already realised at that point that there was a subtle trick which would utilise the power supply's capabilities better and improve the result - the ARB:REP feature. The ARB:REP command tells the supply how many times to loop the arbitrary waveform data table before either holding the last data point or switching off the output (default). This value can be as high as 65535 times, with zero denoting loop infinitely. If I use this feature, I don't have to precisely time an "OUTP 0" command to turn the output off - in fact, I can just write the next arbitrary data set into memory and load up the channel for the next note. This makes the regularity of playback better and reduces the command processing load. The reason I didn't use this in the first revision was because I didn't take note of the example in the manual carefully enough - ARB:REP must be defined before ARB:TRAN (copy to channel) otherwise it is ignored. That's why I couldn't get this working the first time.
One big benefit of reduced processing load was being able to play the song at its regulat speed, rather than half speed. It sounds a lot more like a game than a ... games console running out of power ...
The next thing I thought of was just writing the whole song into the ARB table. The NGM202 has a capacious 4096-point ARB table, which could actually store most of the song. However, unfortunately, after modifying the code to output an ARB table dataset, it turns out I need 4372 points to complete the song. I could have split the song into several tables and loaded one, then the other with the advantage that the playback will be basically reliant on the power supply's own internals throughout the two halves, but I decided to forego this for now.
Instead, I looked towards issues which could affect playback regularity - one of which is jitter in the transmission of commands. As I was running over Ethernet for convenience, there is a penalty for this in terms of performance and jitter as the link is shared with other data and special encoding/decoding/checking and buffering may take place. Instead, those who are familiar with SCPI instruments will know that USB is much faster latency-wise and has a relatively consistent timing, or low jitter. Changing to USB actually made a big difference to the consistency of playback - instead of several dropped notes, it was only two ...
Improving the Sound
I realised that some viewers may not like the on-camera sound which picked up quite a bit of fan noise from my workstation PC. This time I put a Zoom H1 Handy Recorder behind the computer speaker and cranked the volume a little - the result is a much cleaner and crisper sound.
The Code
The Python code is below - it comes without any warranties and requires pyvisa. The VISA resource identifier should be changed to match your device. The standard disclaimer applies - I won't be held responsible for any damage which may be incurred from your use, misuse or inability to use this code. The code is particularly harsh on the power supply's output relays and regulation circuitry - use at your own risk!
# Mario Theme on a R&S NGM202 Power Supply - v2 # Updated to use pulse-wave rather than square wave, # exploiting the ARB:REP feature for timing. # by Gough Lui (goughlui.com) - March 2020 # # Adapted from # https://gist.github.com/gskielian/6135641 # # No warranties. Code depends on pyvisa. Change SCPI resource identifier to match your device. # No liability accepted for damages however incurred. import visa import time def tone (c,f,t) : dura = "{:4e}".format((8/f)-0.001) # Scale down frequency by 4, half period, scientific format string ins_ngm202.write("ARB:DATA "+str(outvolt)+","+str(outcur)+",0.001,0,0.0,"+str(outcur)+","+dura+",0") ins_ngm202.write("ARB:REP "+str(int((t*f)/4000))) # Number of Cycles Needed ins_ngm202.write("ARB:TRAN "+str(outch)) ins_ngm202.write("ARB 1") ins_ngm202.write("OUTP 1") ins_ngm202.query("*OPC?") time.sleep(t/1000) def delay (t) : time.sleep(t/1000) resource_manager = visa.ResourceManager() ins_ngm202 = resource_manager.open_resource("USB0::0x0AAD::0x0197::3638.4472k03-100856::INSTR") ins_ngm202.timeout = 10000 outvolt = 0.75 outcur = 0.5 outch = 1 print("Available:" + "\n" + ins_ngm202.query("*IDN?")) input("Play Mario Theme?") # Set Up NGM202 print("Setting Up - NGM202") ins_ngm202.write("INST:NSEL "+str(outch)) ins_ngm202.write("SENS:VOLT:RANG:AUTO 0") ins_ngm202.write("SENS:VOLT:RANG 5") ins_ngm202.write("SENS:CURR:RANG:AUTO 0") ins_ngm202.write("SENS:CURR:RANG 1") ins_ngm202.write("OUTP 0") ins_ngm202.write("OUTP:GEN 0") ins_ngm202.write("OUTP:MODE SOUR") ins_ngm202.write("SOUR:VOLT 0.0") ins_ngm202.write("SOUR:CURR "+str(outcur)) ins_ngm202.query("*OPC?") # Begin Playback - Code is Arduino Code Directly Taken! # Lucky Python ignores all semi-colons and I have written tone() and delay() # functions to take care of the code without needing modification. print("Begin Melody") tone(9,660,100); delay(150); tone(9,660,100); delay(300); tone(9,660,100); delay(300); tone(9,510,100); delay(100); tone(9,660,100); delay(300); tone(9,770,100); delay(550); tone(9,380,100); delay(575); tone(9,510,100); delay(450); tone(9,380,100); delay(400); tone(9,320,100); delay(500); tone(9,440,100); delay(300); tone(9,480,80); delay(330); tone(9,450,100); delay(150); tone(9,430,100); delay(300); tone(9,380,100); delay(200); tone(9,660,80); delay(200); tone(9,760,50); delay(150); tone(9,860,100); delay(300); tone(9,700,80); delay(150); tone(9,760,50); delay(350); tone(9,660,80); delay(300); tone(9,520,80); delay(150); tone(9,580,80); delay(150); tone(9,480,80); delay(500); tone(9,510,100); delay(450); tone(9,380,100); delay(400); tone(9,320,100); delay(500); tone(9,440,100); delay(300); tone(9,480,80); delay(330); tone(9,450,100); delay(150); tone(9,430,100); delay(300); tone(9,380,100); delay(200); tone(9,660,80); delay(200); tone(9,760,50); delay(150); tone(9,860,100); delay(300); tone(9,700,80); delay(150); tone(9,760,50); delay(350); tone(9,660,80); delay(300); tone(9,520,80); delay(150); tone(9,580,80); delay(150); tone(9,480,80); delay(500); tone(9,500,100); delay(300); tone(9,760,100); delay(100); tone(9,720,100); delay(150); tone(9,680,100); delay(150); tone(9,620,150); delay(300); tone(9,650,150); delay(300); tone(9,380,100); delay(150); tone(9,430,100); delay(150); tone(9,500,100); delay(300); tone(9,430,100); delay(150); tone(9,500,100); delay(100); tone(9,570,100); delay(220); tone(9,500,100); delay(300); tone(9,760,100); delay(100); tone(9,720,100); delay(150); tone(9,680,100); delay(150); tone(9,620,150); delay(300); tone(9,650,200); delay(300); tone(9,1020,80); delay(300); tone(9,1020,80); delay(150); tone(9,1020,80); delay(300); tone(9,380,100); delay(300); tone(9,500,100); delay(300); tone(9,760,100); delay(100); tone(9,720,100); delay(150); tone(9,680,100); delay(150); tone(9,620,150); delay(300); tone(9,650,150); delay(300); tone(9,380,100); delay(150); tone(9,430,100); delay(150); tone(9,500,100); delay(300); tone(9,430,100); delay(150); tone(9,500,100); delay(100); tone(9,570,100); delay(420); tone(9,585,100); delay(450); tone(9,550,100); delay(420); tone(9,500,100); delay(360); tone(9,380,100); delay(300); tone(9,500,100); delay(300); tone(9,500,100); delay(150); tone(9,500,100); delay(300); tone(9,500,100); delay(300); tone(9,760,100); delay(100); tone(9,720,100); delay(150); tone(9,680,100); delay(150); tone(9,620,150); delay(300); tone(9,650,150); delay(300); tone(9,380,100); delay(150); tone(9,430,100); delay(150); tone(9,500,100); delay(300); tone(9,430,100); delay(150); tone(9,500,100); delay(100); tone(9,570,100); delay(220); tone(9,500,100); delay(300); tone(9,760,100); delay(100); tone(9,720,100); delay(150); tone(9,680,100); delay(150); tone(9,620,150); delay(300); tone(9,650,200); delay(300); tone(9,1020,80); delay(300); tone(9,1020,80); delay(150); tone(9,1020,80); delay(300); tone(9,380,100); delay(300); tone(9,500,100); delay(300); tone(9,760,100); delay(100); tone(9,720,100); delay(150); tone(9,680,100); delay(150); tone(9,620,150); delay(300); tone(9,650,150); delay(300); tone(9,380,100); delay(150); tone(9,430,100); delay(150); tone(9,500,100); delay(300); tone(9,430,100); delay(150); tone(9,500,100); delay(100); tone(9,570,100); delay(420); tone(9,585,100); delay(450); tone(9,550,100); delay(420); tone(9,500,100); delay(360); tone(9,380,100); delay(300); tone(9,500,100); delay(300); tone(9,500,100); delay(150); tone(9,500,100); delay(300); tone(9,500,60); delay(150); tone(9,500,80); delay(300); tone(9,500,60); delay(350); tone(9,500,80); delay(150); tone(9,580,80); delay(350); tone(9,660,80); delay(150); tone(9,500,80); delay(300); tone(9,430,80); delay(150); tone(9,380,80); delay(600); tone(9,500,60); delay(150); tone(9,500,80); delay(300); tone(9,500,60); delay(350); tone(9,500,80); delay(150); tone(9,580,80); delay(150); tone(9,660,80); delay(550); tone(9,870,80); delay(325); tone(9,760,80); delay(600); tone(9,500,60); delay(150); tone(9,500,80); delay(300); tone(9,500,60); delay(350); tone(9,500,80); delay(150); tone(9,580,80); delay(350); tone(9,660,80); delay(150); tone(9,500,80); delay(300); tone(9,430,80); delay(150); tone(9,380,80); delay(600); tone(9,660,100); delay(150); tone(9,660,100); delay(300); tone(9,660,100); delay(300); tone(9,510,100); delay(100); tone(9,660,100); delay(300); tone(9,770,100); delay(550); tone(9,380,100); delay(575); print("Song End. Closing instrument!") ins_ngm202.write("OUTP 0") ins_ngm202.write("OUTP:GEN 0") ins_ngm202.write("ARB 0") ins_ngm202.close()
Video
The improved version sounds like this - a lot more pleasant I'd say.
Going the ARB Table .csv Way
For giggles, I tried generating .csv files in the NGM202 format for playback on the internal QuickArb function. It worked well, without the same wear on the relays between notes, as it maintains the output on at a voltage of zero. The song had to be split into two files - one file has most of the song, the last only has the ending part. This is a much less painful way to demo sounds on the power supply - just load the .csv onto a USB stick and load it into the channel, attach a speaker et voila! See attachments for the .csv files.
Conclusion
Sometimes, we can conceive ideas and make it real. But the first time is often just one step towards a solution - in this case, I proved that my simple, naive code approach did not take best advantage of what the hardware had to offer, and with some simple tweaks, a better result emerged. The hardware is still the same - just a little difference in code and connectivity. Perhaps if you were content with only part of the song, loading it as an ARB table via SCPI or .csv would be a way to ensure practically "seamless" playback of tunes that exploit the limited low-frequency pulse-wave capabilities of this instrument.
Top Comments