Acoustics

Enter Your Electronics & Design Project for a chance to win an $200 Shopping cart of product!

Back to The Project14 homepage

Project14 Home
Monthly Themes
Monthly Theme Poll

 

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.