Synthesizing Chords with /dev/audio on Linux

  • Thread starter Thread starter Strilanc
  • Start date Start date
Click For Summary

Discussion Overview

The discussion revolves around synthesizing audio chords using the /dev/audio interface on Linux. Participants explore issues related to sound quality when playing multiple notes simultaneously, focusing on the technical aspects of wave combination and audio processing in a programming context.

Discussion Character

  • Technical explanation
  • Debate/contested
  • Mathematical reasoning

Main Points Raised

  • One participant reports that while single notes sound appropriate, chords produce a garbled output, suggesting issues with how waves are combined.
  • Another participant questions whether increasing the sample rate would improve sound quality.
  • A participant mentions that increasing the sample rate did not resolve the garbled sound and provides additional observations about the sound quality with different numbers of notes.
  • Concerns about potential clipping in the audio signal are raised, along with suggestions for spectral analysis and waveform plotting.
  • One participant proposes that the amplitude of the resulting sound wave may not be scaled correctly, referencing their experience with audio synthesis.
  • Another participant emphasizes the importance of handling negative numbers correctly in the audio signal processing.
  • Discussion includes a suggestion to use a different audio interface (ossaudiodev) for better handling of sound data.
  • A participant describes their attempts to implement changes based on previous suggestions but continues to experience issues with sound quality.
  • Finally, a participant identifies a bug in their code that significantly improved the audio output.

Areas of Agreement / Disagreement

Participants express various hypotheses about the causes of the garbled sound, including issues with amplitude scaling and signal processing. There is no consensus on a single solution, and multiple competing views remain regarding the best approach to synthesizing chords.

Contextual Notes

Participants mention limitations in their background knowledge of signal processing, which may affect their ability to diagnose and resolve the audio issues. There are also unresolved questions about the correct handling of audio formats and the expected input for /dev/audio.

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: 470
  • digital.png
    digital.png
    12 KB · Views: 440
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 6 ·
Replies
6
Views
4K
  • · Replies 264 ·
9
Replies
264
Views
33K