Răsfoiți Sursa

Experiment PCM ready

Grissess 9 ani în urmă
părinte
comite
40b1e56164
3 a modificat fișierele cu 88 adăugiri și 23 ștergeri
  1. 28 1
      broadcast.py
  2. 59 22
      client.py
  3. 1 0
      packet.py

+ 28 - 1
broadcast.py

@@ -37,12 +37,14 @@ parser.add_option('-B', '--bind-addr', dest='bind_addr', help='The IP address (o
 parser.add_option('--repeat', dest='repeat', action='store_true', help='Repeat the file playlist indefinitely')
 parser.add_option('-n', '--number', dest='number', type='int', help='Number of clients to use; if negative (default -1), use the product of stream count and the absolute value of this parameter')
 parser.add_option('--dry', dest='dry', action='store_true', help='Dry run--don\'t actually search for or play to clients, but pretend they exist (useful with -G)')
+parser.add_option('--pcm', dest='pcm', action='store_true', help='Use experimental PCM rendering')
+parser.add_option('--pcm-lead', dest='pcmlead', type='float', help='Seconds of leading PCM data to send')
 parser.add_option('-G', '--gui', dest='gui', default='', help='set a GUI to use')
 parser.add_option('--pg-fullscreen', dest='fullscreen', action='store_true', help='Use a full-screen video mode')
 parser.add_option('--pg-width', dest='pg_width', type='int', help='Width of the pygame window')
 parser.add_option('--pg-height', dest='pg_height', type='int', help='Width of the pygame window')
 parser.add_option('--help-routes', dest='help_routes', action='store_true', help='Show help about routing directives')
-parser.set_defaults(routes=[], test_delay=0.25, random=0.0, rand_low=80, rand_high=2000, live=None, factor=1.0, duration=1.0, volume=1.0, wait_time=0.25, play=[], transpose=0, seek=0.0, bind_addr='', pg_width = 0, pg_height = 0, number=-1)
+parser.set_defaults(routes=[], test_delay=0.25, random=0.0, rand_low=80, rand_high=2000, live=None, factor=1.0, duration=1.0, volume=1.0, wait_time=0.25, play=[], transpose=0, seek=0.0, bind_addr='', pg_width = 0, pg_height = 0, number=-1, pcmlead=0.1)
 options, args = parser.parse_args()
 
 if options.help_routes:
@@ -314,6 +316,31 @@ if options.repeat:
     args = itertools.cycle(args)
 
 for fname in args:
+    if options.pcm and not fname.endswith('.iv'):
+        try:
+            import audiotools
+            pcr = audiotools.open(fname).to_pcm()
+            assert pcr.channels == 1 and pcr.bits_per_sample == 16 and pcr.sample_rate == 44100
+        except ImportError:
+            import wave
+            pcr = wave.open(fname, 'r')
+            assert pcr.getnchannels() == 1 and pcr.getsampwidth() == 2 and pcr.getframerate() == 44100
+
+        BASETIME = time.time() - options.pcmlead
+        sampcnt = 0
+        buf = pcr.read(16).to_bytes(False, True)
+        while buf:
+            frag = buf[:32]
+            buf = buf[32:]
+            for cl in clients:
+                s.sendto(struct.pack('>L', CMD.PCM) + frag, cl)
+            sampcnt += len(frag) / 2
+            delay = max(0, BASETIME + (sampcnt / float(pcr.sample_rate)) - time.time())
+            #print sampcnt, delay
+            if delay > 0:
+                time.sleep(delay)
+            if not buf:
+                buf = pcr.read(16).to_bytes(False, True)
     try:
         iv = ET.parse(fname).getroot()
     except IOError:

+ 59 - 22
client.py

@@ -25,6 +25,7 @@ parser.add_option('-p', '--port', dest='port', type='int', default=13676, help='
 parser.add_option('-r', '--rate', dest='rate', type='int', default=44100, help='Set the sample rate of the audio device')
 parser.add_option('-V', '--volume', dest='volume', type='float', default=1.0, help='Set the volume factor (>1 distorts, <1 attenuates)')
 parser.add_option('-n', '--streams', dest='streams', type='int', default=1, help='Set the number of streams this client will play back')
+parser.add_option('-N', '--numpy', dest='numpy', action='store_true', help='Use numpy acceleration')
 parser.add_option('-G', '--gui', dest='gui', default='', help='set a GUI to use')
 parser.add_option('--pg-fullscreen', dest='fullscreen', action='store_true', help='Use a full-screen video mode')
 parser.add_option('--pg-samp-width', dest='samp_width', type='int', help='Set the width of the sample pane (by default display width / 2)')
@@ -33,6 +34,9 @@ parser.add_option('--pg-height', dest='height', type='int', help='Set the height
 
 options, args = parser.parse_args()
 
+if options.numpy:
+    import numpy
+
 PORT = options.port
 STREAMS = options.streams
 IDENT = 'TONE'
@@ -51,6 +55,7 @@ AMPS = [MAX] * STREAMS
 MIN = -0x80000000
 
 EXPIRATIONS = [0] * STREAMS
+QUEUED_PCM = ''
 
 def lin_interp(frm, to, p):
     return p*to + (1-p)*frm
@@ -282,30 +287,57 @@ generator = eval(options.generator)
 #    global FREQ
 #    FREQ = 0
 
-def lin_seq(frm, to, cnt):
-    step = (to-frm)/float(cnt)
-    samps = [0]*cnt
-    for i in xrange(cnt):
-        p = i / float(cnt-1)
-        samps[i] = int(lin_interp(frm, to, p))
-    return samps
-
-def samps(freq, amp, phase, cnt):
-    global RATE
-    samps = [0]*cnt
-    for i in xrange(cnt):
-        samps[i] = int(amp / math.sqrt(STREAMS) * max(-1, min(1, options.volume*generator((phase + 2 * math.pi * freq * i / RATE) % (2*math.pi)))))
-    return samps, (phase + 2 * math.pi * freq * cnt / RATE) % (2*math.pi)
-
-def to_data(samps):
-    return struct.pack('i'*len(samps), *samps)
-
-def mix(a, b):
-    return [min(MAX, max(MIN, i + j)) for i, j in zip(a, b)]
+if options.numpy:
+    lin_seq = numpy.linspace
+
+    def samps(freq, amp, phase, cnt):
+        samps = numpy.ndarray((cnt,), numpy.int32)
+        pvel = 2 * math.pi * freq / RATE
+        fac = amp / float(STREAMS)
+        for i in xrange(cnt):
+            samps[i] = fac * max(-1, min(1, generator(phase)))
+            phase = (phase + pvel) % (2 * math.pi)
+        return samps, phase
+
+    def to_data(samps):
+        return samps.tobytes()
+
+    def mix(a, b):
+        return a + b
+
+else:
+    def lin_seq(frm, to, cnt):
+        step = (to-frm)/float(cnt)
+        samps = [0]*cnt
+        for i in xrange(cnt):
+            p = i / float(cnt-1)
+            samps[i] = int(lin_interp(frm, to, p))
+        return samps
+
+    def samps(freq, amp, phase, cnt):
+        global RATE
+        samps = [0]*cnt
+        for i in xrange(cnt):
+            samps[i] = int(amp / math.sqrt(STREAMS) * max(-1, min(1, options.volume*generator((phase + 2 * math.pi * freq * i / RATE) % (2*math.pi)))))
+        return samps, (phase + 2 * math.pi * freq * cnt / RATE) % (2*math.pi)
+
+    def to_data(samps):
+        return struct.pack('i'*len(samps), *samps)
+
+    def mix(a, b):
+        return [min(MAX, max(MIN, i + j)) for i, j in zip(a, b)]
 
 def gen_data(data, frames, tm, status):
-    global FREQS, PHASE, Z_SAMP, LAST_SAMP, LAST_SAMPLES
-    fdata = [0] * frames
+    global FREQS, PHASE, Z_SAMP, LAST_SAMP, LAST_SAMPLES, QUEUED_PCM
+    if len(QUEUED_PCM) >= frames*4:
+        fdata = QUEUED_PCM[:frames*4]
+        QUEUED_PCM = QUEUED_PCM[frames*4:]
+        LAST_SAMPLES.extend(struct.unpack(str(frames)+'i', fdata))
+        return fdata, pyaudio.paContinue
+    if options.numpy:
+        fdata = numpy.zeros((frames,), numpy.int32)
+    else:
+        fdata = [0] * frames
     for i in range(STREAMS):
         FREQ = FREQS[i]
         LAST_SAMP = LAST_SAMPS[i]
@@ -384,5 +416,10 @@ while True:
         for i in xrange(len(UID)/4):
             data[i+2] = stoi(UID[4*i:4*(i+1)])
         sock.sendto(str(Packet(CMD.CAPS, *data)), cli)
+    elif pkt.cmd == CMD.PCM:
+        fdata = data[4:]
+        fdata = struct.pack('16i', *[i<<16 for i in struct.unpack('16h', fdata)])
+        QUEUED_PCM += fdata
+        print 'Now', len(QUEUED_PCM) / 4.0, 'frames queued'
     else:
         print 'Unknown cmd', pkt.cmd

+ 1 - 0
packet.py

@@ -24,6 +24,7 @@ class CMD:
 	QUIT = 2 # No important data
 	PLAY = 3 # seconds, microseconds, frequency (Hz), amplitude (0.0 - 1.0), port
         CAPS = 4 # ports, client type (1), user ident (2-7)
+        PCM = 5 # 16 samples, encoded S16_LE
 
 def itos(i):
     return struct.pack('>L', i)