Synthesizing Chords with /dev/audio on Linux

  • Thread starter Thread starter Strilanc
  • Start date Start date
AI Thread Summary
The discussion revolves around generating audio chords using /dev/audio on a Linux system, with the user encountering issues when playing multiple notes simultaneously. While single notes sound correct, chords produce garbled audio. The user suspects the problem lies in how they combine the waveforms of the notes, as they are simply adding them together without proper scaling. Key points include the need to handle negative values correctly and to avoid clipping by ensuring that the combined amplitude remains within a certain range. Suggestions include dividing the amplitude by the number of notes to prevent distortion and ensuring the output signal is in the expected format for /dev/audio. The user transitions to using the ossaudiodev library to manage audio output more effectively, but still faces challenges with static noise, even at a higher sample rate. The discussion highlights the importance of proper signal processing techniques, including amplitude scaling and waveform representation, to achieve clear audio playback.
Strilanc
Science Advisor
Messages
612
Reaction score
229
I'm trying to play chords using /dev/audio on my linux box. The idea is, given a set of notes, I generate the audio wave created and pass it to /dev/audio (which plays it through the speakers).

The problem is it's not working. Single notes sound appropriate, but chords sound wrong. What comes out is some kind of garbled I-don't-know-what.

I believe the issue is how I'm combining the waves made by the notes. I just add them together. Am I supposed to use some other transformation?

Here is my source code (python):
Code:
import math
import random

#constants
midC = 60
midA = midC + 9
sampleRate = 8000

#returns true with probability p
def chance(p):
	return random.uniform(0,1) < p

#returns the frequency of the given note
def noteFreq(note):
	note -= midA
	return pow(2.0, note/12.) * 440.

#converts amplitudes to a character sequence for /dev/audio
def digitize(amplitudes):
	s = ""
	for d in amplitudes:
		d = abs(d)
		if chance(d%1.): d+=1 #anti-alias
		s += chr(int(min(d,255)))
	return s

#Plays notes through /dev/audio
class NotePlayer:
	def __init__(self, amplitude=63, dt=0.4):
		self.dt = dt
		self.amplitude = amplitude
		self.audio = file('/dev/audio', 'wb')
		self.t = 0

	def __del__(self):
		self.audio.close()

	#play a set of notes
	def playNotes(self, notes):
		freqs = map(noteFreq, notes)
		data = []
		tf = self.t + int(self.dt*sampleRate)

		#compute the wave over a time dt, from t to t-final
		for i in range(self.t, tf):
			x = (i+self.t)*math.pi/sampleRate #the sample time
			#combine the waves to get a sample
			d = 0
			for f in freqs:
				d += math.sin(x*f)
			d *= self.amplitude
			data += [d]
		
		#play the sound
		self.audio.write(digitize(data))
		self.t = tf

	#plays a single note
	def playNote(self, note):
		self.playNotes((note,))

#The actual run-time code
p = NotePlayer()
while 1==1:
	p.playNotes((midC,midC+4,midC+7)) #play the major chord C-E-G
        #sounds really bad
	#ctrl+c to kill
 
Last edited:
Technology news on Phys.org
Do things improve if you increase sampleRate?
 
The sampleRate variable represents how quickly /dev/audio plays the data I give it. Technically I can't modify it because then I give data to /dev/audio sampled at an incorrect rate.

However, you might be right, so I tried increasing the sample rate by a factor of 4. The pitch lowered (of course), but the garbling noise was still loud and clear. I don't think the sampling rate is the issue.

A little more information about what is going wrong:
- 1 note: seems fine
- 2 notes: plays well, but seems not loud enough
- 3 notes and beyond: garbled mess
 
Last edited:
Could there be some clipping going on?
Does the synthesized waveform look correct?
Can you do a spectral analysis on the waveform?
 
How do I do those things? Keep in mind I don't have any background in signal processing or things of that nature.

I'll try to cobble something together to plot the sine wave.
 
Here are shots of the analog and digitized wave for C-E-G.

Am I handling negative numbers correctly?
 

Attachments

  • analog.png
    analog.png
    11.6 KB · Views: 443
  • digital.png
    digital.png
    12 KB · Views: 415
My guess is that the garbling is due to you not scaling the amplitude of the resulting sound wave to a reasonable level. I'm using Max/MSP for a project, and I know I have to keep the amplitude between -1 and +1 or else there'll be distortion. If you just play sin(440x), you have something which oscillates between -1 and +1, and so you're fine, but if you then add sin(880x), you have something which varies between -2 and +2, and you're bound to hear distortion. Try dividing your amplitude by 3 after making the chord.
 
JoAuSc describes what I called "clipping" above.

Strilanc, I don't think that you should take the absolute value of the synthesized waveform. Did you do this in the single tone case??

Did you try to do the synthesis in (say) audacity?
 
  • #10
Dividing by the number of notes gave a very large improvement. I can recognize tunes now.

There is still garble. Changing the value to the sum of absolutes instead of the absolute of the sums helped a little more. I'm guessing this is because of me still combining the signals incorrectly.

I think I need to figure out how to give negative values to /dev/audio so I don't need to use absolute.
 
  • #11
1. Find the largest negative number. Add its absolute-value to each waveform point.
2. If your sum ever exceeds 255, you have clipping... so you might want to then multiply each by 255./max (where max is the maximum value after step 1).
 
  • #12
Adding a constant of 128 to the wave causes no audible sound to come from the speakers. I'm not sure how /dev/audio interprets what I write to it.

In any case, the playback doesn't need to be great. I only needed to be able to hear the notes. I'm writing a Markov Babbler, which generates music based on note-following probabilities from existing music.
 
  • #13
1) You are not handling negative numbers properly. You need to produce a signal in the range -128 to +127, convert it to an integer, and add 128 to it. That will produce a signal that ranges from 0 to 255. Obviously, your "analog" and "digital" waveforms should look the same, just with different numbers along the y-axis -- but they do not.

2) What you're currently creating is called PCM (pulse-code modulated) data. Your /dev/audio device by default expects mu-law encoding, not PCM. It won't ever sound right unless you reconfigure it to expect PCM data.

3) Consider using a better interface to /dev/audio than just throwing bytes at it. Try ossaudiodev, http://docs.python.org/lib/ossaudio-device-objects.html You should be able to set /dev/audio to accept PCM data first, and then it should be able to handle your sound data properly.

- Warren
 
Last edited by a moderator:
  • #14
I changed over to ossaudiodev. I'm now having more issues: even single notes sound wrong. I don't understand what I'm doing incorrectly.

- I set up /dev/audio to use PCM (also tried other formats to see if that worked)
- I compute the signal as the sum of the sine waves of the notes, divided by the number of waves
- I sample the signal, convert it to a series of whole numbers on [0,256) by adding a constant of 128 and rounding
- I pass these to /dev/audio

Code:
import math
import random
import ossaudiodev

#constants
midC = 60
midA = midC + 9

#returns true with probability p
def chance(p):
	return random.uniform(0,1) < p

#returns the frequency of the given note
def noteFreq(note):
	note -= midA
	return pow(2.0, note/12.) * 440.
	
#Plays notes through /dev/audio
class NotePlayer:
	def __init__(self, amplitude=50, notePeriod=1.0, sampleRate=8000):
		self.notePeriod = notePeriod
		self.amplitude = amplitude
		self.sampleRate = sampleRate
		self.t = 0

		self.audio = ossaudiodev.open('/dev/audio', 'w')
		self.audio.setfmt(ossaudiodev.AFMT_IMA_ADPCM)
		self.audio.speed(self.sampleRate)

	def __del__(self):
		self.audio.close()

	#digitizes the given analog amplitude
	def digitize(self, d):
		d += 128
		if chance(d%1.): d+=1 #anti-alias
		d = max(d, 0)
		d = min(d, 255)
		return int(d)

	#passes amplitudes to /dev/audio as a character sequence
	def playSignal(self, signal):
		s = ""
		data = map(self.digitize, signal)
		for d in data:
			s += chr(d)
		self.audio.write(s)

	#plays a set of notes
	def playNotes(self, notes):
		dt = int(self.notePeriod*self.sampleRate)
		data = []
		freqs = map(noteFreq, notes)

		#compute the signal
		for i in range(self.t, self.t + dt):
			d = 0
			if len(freqs) > 0:
				x = (i+self.t)*math.pi/self.sampleRate
				for f in freqs:
					d += math.sin(x*f)
				d *= self.amplitude/len(freqs)
			data += [d]
		
		#output the signal
		self.playSignal(data)
		self.t += dt

#actual run-time
p = NotePlayer()
while 1==1:
	notes = ((midC,))
	p.playNotes(notes)
	notes = ((midC,midC+7))
	p.playNotes(notes)
        #ctrl+c to kill
 
  • #15
I found a silly bug that fixed a lot of the issues.

Code:
x = (i+self.t)*math.pi/self.sampleRate //self.t already included in i!

However, I still hear static in the background. Surprisingly (at least to me), the static gets worse if I increase the sample rate. It is present even for single notes.
 
Last edited:

Similar threads

Replies
264
Views
31K
Back
Top