]> git.sesse.net Git - vlc/blob - modules/access/qtsound.m
Qt4: assume that items are appended to end, so start search from there
[vlc] / modules / access / qtsound.m
1 /*****************************************************************************
2 * qtsound.m: qtkit (Mac OS X) based audio capture module
3 *****************************************************************************
4 * Copyright © 2011 VLC authors and VideoLAN
5 *
6 * Authors: Pierre d'Herbemont <pdherbemont@videolan.org>
7 *          Gustaf Neumann <neumann@wu.ac.at>
8 *          Michael S. Feurstein <michael.feurstein@wu.ac.at>
9 *
10 *****************************************************************************
11 * This library is free software; you can redistribute it and/or
12 * modify it under the terms of the GNU Lesser General Public License
13 * as published by the Free Software Foundation; either version 2.1
14 * of the License, or (at your option) any later version.
15 *
16 * This library is distributed in the hope that it will be useful,
17 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
19 * Lesser General Public License for more details.
20 *
21 * You should have received a copy of the GNU Lesser General Public
22 * License along with this library; if not, write to the Free Software
23 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110, USA
24 *
25 *****************************************************************************/
26
27 /*****************************************************************************
28  * Preamble
29  *****************************************************************************/
30
31 #ifdef HAVE_CONFIG_H
32 # include "config.h"
33 #endif
34
35 #include <vlc_common.h>
36 #include <vlc_plugin.h>
37 #include <vlc_aout.h>
38
39 #include <vlc_demux.h>
40 #include <vlc_dialog.h>
41
42 //#define QTKIT_VERSION_MIN_REQUIRED 70603
43 #define QTKIT_VERSION_MAX_ALLOWED 70700
44
45 #import <QTKit/QTKit.h>
46
47 /*****************************************************************************
48  * Local prototypes.
49  *****************************************************************************/
50 static int Open( vlc_object_t *p_this );
51 static void Close( vlc_object_t *p_this );
52 static int Demux( demux_t *p_demux );
53 static int Control( demux_t *, int, va_list );
54
55 /*****************************************************************************
56  * Module descriptor
57  *****************************************************************************/
58
59 vlc_module_begin()
60 set_shortname( N_("QTSound") )
61 set_description( N_("QuickTime Sound Capture") )
62 set_category( CAT_INPUT )
63 set_subcategory( SUBCAT_INPUT_ACCESS )
64 add_shortcut( "qtsound" )
65 set_capability( "access_demux", 0 )
66 set_callbacks( Open, Close )
67 vlc_module_end ()
68
69
70 /*****************************************************************************
71  * QTKit Bridge
72  *****************************************************************************/
73 @interface VLCDecompressedAudioOutput : QTCaptureDecompressedAudioOutput
74 {
75     demux_t *p_qtsound;
76     AudioBuffer *currentAudioBuffer;
77     UInt32 numberOfSamples;
78     date_t date;
79     mtime_t currentPts;
80     mtime_t previousPts;
81 }
82 - (id)initWithDemux:(demux_t *)p_demux;
83 - (void)outputAudioSampleBuffer:(QTSampleBuffer *)sampleBuffer fromConnection:(QTCaptureConnection *)connection;
84 - (BOOL)checkCurrentAudioBuffer;
85 - (mtime_t)getCurrentPts;
86 - (void *)getCurrentAudioBufferData;
87 - (UInt32)getCurrentTotalDataSize;
88 - (UInt32)getNumberOfSamples;
89
90 @end
91
92 @implementation VLCDecompressedAudioOutput : QTCaptureDecompressedAudioOutput
93 - (id)initWithDemux:(demux_t *)p_demux
94 {
95     if( self = [super init] )
96     {
97         p_qtsound = p_demux;
98         currentAudioBuffer = nil;
99         date_Init(&date, 44100, 1);
100         date_Set(&date,0);
101         currentPts = 0;
102         previousPts = 0;
103     }
104     return self;
105 }
106 - (void)dealloc
107 {
108     @synchronized (self)
109     {
110         free(currentAudioBuffer);
111         currentAudioBuffer = nil;
112     }
113     [super dealloc];
114 }
115
116 - (void)outputAudioSampleBuffer:(QTSampleBuffer *)sampleBuffer fromConnection:(QTCaptureConnection *)connection
117 {
118     AudioBufferList *tempAudioBufferList;
119     block_t *rawAudioData;
120     UInt32 totalDataSize = 0;
121     UInt32 count = 0;
122
123     @synchronized (self)
124     {
125         numberOfSamples = [sampleBuffer numberOfSamples];
126         date_Increment(&date,numberOfSamples);
127         currentPts = date_Get(&date);
128
129         tempAudioBufferList = [sampleBuffer audioBufferListWithOptions:0];
130         if (tempAudioBufferList->mNumberBuffers == 2)
131         {
132             /*
133              * Compute totalDataSize as sum of all data blocks in the
134              * audio buffer list:
135              */
136             for ( count = 0; count < tempAudioBufferList->mNumberBuffers; count++ )
137             {
138                 totalDataSize += tempAudioBufferList->mBuffers[count].mDataByteSize;
139             }
140             /*
141              * Allocate storage for the interleaved audio data
142              */
143             rawAudioData = block_Alloc(totalDataSize * sizeof(float));
144             if (NULL == rawAudioData)
145             {
146                 msg_Err( p_qtsound, "Raw audiodata could not be allocated" );
147                 return;
148             }
149         }
150         else
151         {
152             msg_Err( p_qtsound, "Too many or only one channel found." );
153             return;
154         }
155
156         /*
157          * Interleave raw data (provided in two separate channels as
158          * F32L) with 2 samples per frame
159          */
160         if ( totalDataSize )
161         {
162             unsigned short i;
163             const float *b1Ptr, *b2Ptr;
164             float *uPtr;
165
166             for (i = 0,
167                  uPtr = (float *)rawAudioData,
168                  b1Ptr = (const float *) tempAudioBufferList->mBuffers[0].mData,
169                  b2Ptr = (const float *) tempAudioBufferList->mBuffers[1].mData;
170                  i < numberOfSamples; i++)
171             {
172                 *uPtr = *b1Ptr;
173                 uPtr ++;
174                 b1Ptr ++;
175                 *uPtr = *b2Ptr;
176                 uPtr ++;
177                 b2Ptr ++;
178             }
179
180             if (currentAudioBuffer == nil)
181             {
182                 currentAudioBuffer = (AudioBuffer *)malloc(sizeof(AudioBuffer));
183                 if (NULL == currentAudioBuffer)
184                 {
185                     msg_Err( p_qtsound, "AudioBuffer could not be allocated." );
186                     return;
187                 }
188             }
189             currentAudioBuffer->mNumberChannels = 2;
190             currentAudioBuffer->mDataByteSize = totalDataSize;
191             currentAudioBuffer->mData = rawAudioData;
192         }
193         free(rawAudioData);
194     }
195 }
196
197 - (BOOL)checkCurrentAudioBuffer
198 {
199     return (currentAudioBuffer) ? 1 : 0;
200 }
201
202 - (mtime_t)getCurrentPts
203 {
204     /* FIXME: can this getter be minimized? */
205     mtime_t pts;
206
207     if( !currentAudioBuffer || currentPts == previousPts )
208     {
209         return 0;
210     }
211
212     @synchronized (self)
213     {
214         pts = previousPts = currentPts;
215     }
216
217     return (currentAudioBuffer->mData) ? currentPts : 0;
218 }
219
220 - (void *)getCurrentAudioBufferData
221 {
222     return currentAudioBuffer->mData;
223 }
224
225 - (UInt32)getCurrentTotalDataSize
226 {
227     return currentAudioBuffer->mDataByteSize;
228 }
229
230 - (UInt32)getNumberOfSamples
231 {
232     return numberOfSamples;
233 }
234
235 @end
236
237 /*****************************************************************************
238  * Struct
239  *****************************************************************************/
240
241 struct demux_sys_t {
242     QTCaptureSession * session;
243     QTCaptureDevice * audiodevice;
244     VLCDecompressedAudioOutput * audiooutput;
245     es_out_id_t *p_es_audio;
246     int i_audio_max_buffer_size;
247 };
248
249 /*****************************************************************************
250  * Open: initialize interface
251  *****************************************************************************/
252 static int Open( vlc_object_t *p_this )
253 {
254     demux_t *p_demux = (demux_t*)p_this;
255     demux_sys_t *p_sys;
256     es_format_t audiofmt;
257     char *psz_uid = NULL;
258     int audiocodec;
259     bool success;
260     NSString *qtk_curraudiodevice_uid;
261     NSAutoreleasePool *pool;
262     NSArray *myAudioDevices, *audioformat_array;
263     QTFormatDescription *audio_format;
264     QTCaptureDeviceInput *audioInput;
265     NSError *o_returnedAudioError;
266
267     if( p_demux->psz_location && *p_demux->psz_location )
268     {
269         psz_uid = p_demux->psz_location;
270     }
271     msg_Dbg( p_demux, "qtsound uid = %s", psz_uid );
272     qtk_curraudiodevice_uid = [[NSString alloc] initWithFormat:@"%s", psz_uid];
273
274     pool = [[NSAutoreleasePool alloc] init];
275
276     p_demux->p_sys = p_sys = calloc( 1, sizeof( demux_sys_t ) );
277     if( !p_sys )
278         return VLC_ENOMEM;
279
280     msg_Dbg( p_demux, "qtsound : uid = %s", [qtk_curraudiodevice_uid UTF8String]);
281     myAudioDevices = [[[QTCaptureDevice inputDevicesWithMediaType:QTMediaTypeSound] arrayByAddingObjectsFromArray:[QTCaptureDevice inputDevicesWithMediaType:QTMediaTypeMuxed]] retain];
282     if([myAudioDevices count] == 0)
283     {
284         dialog_FatalWait( p_demux, _("No Audio Input device found"),
285                          _("Your Mac does not seem to be equipped with a suitable audio input device."
286                      "Please check your connectors and drivers.") );
287         msg_Err( p_demux, "Can't find any Audio device" );
288
289         goto error;
290     }
291     unsigned iaudio;
292     for(iaudio = 0; iaudio < [myAudioDevices count]; iaudio++){
293         QTCaptureDevice *qtk_audioDevice;
294         qtk_audioDevice = [myAudioDevices objectAtIndex:iaudio];
295         msg_Dbg( p_demux, "qtsound audio %u/%lu localizedDisplayName: %s uniqueID: %s", iaudio, [myAudioDevices count], [[qtk_audioDevice localizedDisplayName] UTF8String], [[qtk_audioDevice uniqueID] UTF8String]);
296         if([[[qtk_audioDevice localizedDisplayName]stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] isEqualToString:qtk_curraudiodevice_uid]){
297             msg_Dbg( p_demux, "Device found" );
298             break;
299         }
300     }
301
302     audioInput = nil;
303     if(iaudio < [myAudioDevices count]){
304         p_sys->audiodevice = [myAudioDevices objectAtIndex:iaudio];
305     }
306     else
307     {
308         /* cannot find designated audio device, fall back to open default audio device */
309         msg_Dbg(p_demux, "Cannot find designated uid audio device as %s. Fall back to open default audio device.", [qtk_curraudiodevice_uid UTF8String]);
310         p_sys->audiodevice = [QTCaptureDevice defaultInputDeviceWithMediaType: QTMediaTypeSound];
311     }
312     if( !p_sys->audiodevice )
313     {
314         dialog_FatalWait( p_demux, _("No audio input device found"),
315                          _("Your Mac does not seem to be equipped with a suitable audio input device."
316                      "Please check your connectors and drivers.") );
317         msg_Err( p_demux, "Can't find any Audio device" );
318
319         goto error;
320     }
321
322     if( ![p_sys->audiodevice open: &o_returnedAudioError] )
323     {
324         msg_Err( p_demux, "Unable to open the audio capture device (%ld)", [o_returnedAudioError code] );
325         goto error;
326     }
327
328     if( [p_sys->audiodevice isInUseByAnotherApplication] == YES )
329     {
330         msg_Err( p_demux, "default audio capture device is exclusively in use by another application" );
331         goto error;
332     }
333     audioInput = [[QTCaptureDeviceInput alloc] initWithDevice: p_sys->audiodevice];
334     if( !audioInput )
335     {
336         msg_Err( p_demux, "can't create a valid audio capture input facility" );
337         goto error;
338     } else {
339         msg_Dbg( p_demux, "created valid audio capture input facility" );
340     }
341
342     p_sys->audiooutput = [[VLCDecompressedAudioOutput alloc] initWithDemux:p_demux];
343     msg_Dbg ( p_demux, "initialized audio output" );
344
345     /* Get the formats */
346     /*
347      FIXME: the format description gathered here does not seem to be the same
348      in comparison to the format description collected from the actual sampleBuffer.
349      This information needs to be updated some other place. For the time being this shall suffice.
350
351      The following verbose output is an example of what is read from the input device during the below block
352      [0x3042138] qtsound demux debug: Audio localized format summary: Linear PCM, 24 bit little-endian signed integer, 2 channels, 44100 Hz
353      [0x3042138] qtsound demux debug: Sample Rate: 44100; Format ID: lpcm; Format Flags: 00000004; Bytes per Packet: 8; Frames per Packet: 1; Bytes per Frame: 8; Channels per Frame: 2; Bits per Channel: 24
354      [0x3042138] qtsound demux debug: Flag float 0 bigEndian 0 signedInt 1 packed 0 alignedHigh 0 non interleaved 0 non mixable 0
355      canonical 0 nativeFloatPacked 0 nativeEndian 0
356
357      However when reading this information from the sampleBuffer during the delegate call from
358      - (void)outputAudioSampleBuffer:(QTSampleBuffer *)sampleBuffer fromConnection:(QTCaptureConnection *)connection;
359      the following data shows up
360      2011-09-23 22:06:03.077 VLC[23070:f103] Audio localized format summary: Linear PCM, 32 bit little-endian floating point, 2 channels, 44100 Hz
361      2011-09-23 22:06:03.078 VLC[23070:f103] Sample Rate: 44100; Format ID: lpcm; Format Flags: 00000029; Bytes per Packet: 4; Frames per Packet: 1; Bytes per Frame: 4; Channels per Frame: 2; Bits per Channel: 32
362      2011-09-23 22:06:03.078 VLC[23070:f103] Flag float 1 bigEndian 0 signedInt 0 packed 1 alignedHigh 0 non interleaved 1 non mixable 0
363      canonical 1 nativeFloatPacked 1 nativeEndian 0
364
365      Note the differences
366      24bit vs. 32bit
367      little-endian signed integer vs. little-endian floating point
368      format flag 00000004 vs. 00000029
369      bytes per packet 8 vs. 4
370      packed 0 vs. 1
371      non interleaved 0 vs. 1 -> this makes a major difference when filling our own buffer
372      canonical 0 vs. 1
373      nativeFloatPacked 0 vs. 1
374
375      One would assume we'd need to feed the (es_format_t)audiofmt with the data collected here.
376      This is not the case. Audio will be transmitted in artefacts, due to wrong information.
377
378      At the moment this data is set manually, however one should consider trying to set this data dynamically
379      */
380     audioformat_array = [p_sys->audiodevice formatDescriptions];
381     audio_format = NULL;
382     for( int k = 0; k < [audioformat_array count]; k++ )
383     {
384         audio_format = (QTFormatDescription *)[audioformat_array objectAtIndex: k];
385
386         msg_Dbg( p_demux, "Audio localized format summary: %s", [[audio_format localizedFormatSummary] UTF8String]);
387         msg_Dbg( p_demux, "Audio format description attributes: %s",[[[audio_format formatDescriptionAttributes] description] UTF8String]);
388
389         AudioStreamBasicDescription asbd = {0};
390         NSValue *asbdValue =  [audio_format attributeForKey:QTFormatDescriptionAudioStreamBasicDescriptionAttribute];
391         [asbdValue getValue:&asbd];
392
393         char formatIDString[5];
394         UInt32 formatID = CFSwapInt32HostToBig (asbd.mFormatID);
395         bcopy (&formatID, formatIDString, 4);
396         formatIDString[4] = '\0';
397
398         /* kept for development purposes */
399 #if 0
400         msg_Dbg( p_demux, "Sample Rate: %.0lf; Format ID: %s; Format Flags: %.8x; Bytes per Packet: %d; Frames per Packet: %d; Bytes per Frame: %d; Channels per Frame: %d; Bits per Channel: %d",
401                 asbd.mSampleRate,
402                 formatIDString,
403                 asbd.mFormatFlags,
404                 asbd.mBytesPerPacket,
405                 asbd.mFramesPerPacket,
406                 asbd.mBytesPerFrame,
407                 asbd.mChannelsPerFrame,
408                 asbd.mBitsPerChannel);
409
410         msg_Dbg( p_demux, "Flag float %d bigEndian %d signedInt %d packed %d alignedHigh %d non interleaved %d non mixable %d\ncanonical %d nativeFloatPacked %d nativeEndian %d",
411                 (asbd.mFormatFlags & kAudioFormatFlagIsFloat) != 0,
412                 (asbd.mFormatFlags & kAudioFormatFlagIsBigEndian) != 0,
413                 (asbd.mFormatFlags & kAudioFormatFlagIsSignedInteger) != 0,
414                 (asbd.mFormatFlags & kAudioFormatFlagIsPacked) != 0,
415                 (asbd.mFormatFlags & kAudioFormatFlagIsAlignedHigh) != 0,
416                 (asbd.mFormatFlags & kAudioFormatFlagIsNonInterleaved) != 0,
417                 (asbd.mFormatFlags & kAudioFormatFlagIsNonMixable) != 0,
418
419                 (asbd.mFormatFlags & kAudioFormatFlagsCanonical) != 0,
420                 (asbd.mFormatFlags & kAudioFormatFlagsNativeFloatPacked) != 0,
421                 (asbd.mFormatFlags & kAudioFormatFlagsNativeEndian) != 0
422                 );
423 #endif
424     }
425
426     if( [audioformat_array count] )
427         audio_format = [audioformat_array objectAtIndex: 0];
428     else goto error;
429
430     /* Now we can init */
431     audiocodec = VLC_CODEC_FL32;
432     es_format_Init( &audiofmt, AUDIO_ES, audiocodec);
433
434     audiofmt.audio.i_format = audiocodec;
435     audiofmt.audio.i_rate = 44100;
436     /*
437      * i_physical_channels Describes the channels configuration of the
438      * samples (ie. number of channels which are available in the
439      * buffer, and positions).
440      */
441     audiofmt.audio.i_physical_channels = AOUT_CHAN_RIGHT | AOUT_CHAN_LEFT;
442     /*
443      * i_original_channels Describes from which original channels,
444      * before downmixing, the buffer is derived.
445      */
446     audiofmt.audio.i_original_channels = AOUT_CHAN_RIGHT | AOUT_CHAN_LEFT;
447     /*
448      * i_bytes_per_frame Optional - for A/52, SPDIF and DTS types:
449      * Bytes used by one compressed frame, depends on bitrate.
450      */
451     audiofmt.audio.i_bytes_per_frame = 4;
452     /*
453      * Number of sampleframes contained in one compressed frame.
454      */
455     audiofmt.audio.i_frame_length = 1;
456     /*
457      * Please note that it may be completely arbitrary - buffers are not
458      * obliged to contain a integral number of so-called "frames". It's
459      * just here for the division:
460      * buffer_size = i_nb_samples * i_bytes_per_frame / i_frame_length
461      */
462     audiofmt.audio.i_bitspersample = 32;
463     audiofmt.audio.i_channels = 2;
464     audiofmt.audio.i_blockalign = audiofmt.audio.i_channels * audiofmt.audio.i_bitspersample / 16;
465     audiofmt.i_bitrate = audiofmt.audio.i_channels * audiofmt.audio.i_rate * audiofmt.audio.i_bitspersample;
466     p_sys->i_audio_max_buffer_size = 4096;
467
468     p_sys->session = [[QTCaptureSession alloc] init];
469
470     success = [p_sys->session addInput:audioInput error: &o_returnedAudioError];
471     if( !success )
472     {
473         msg_Err( p_demux, "the audio capture device could not be added to capture session (%ld)", [o_returnedAudioError code] );
474         goto error;
475     }
476
477     success = [p_sys->session addOutput:p_sys->audiooutput error: &o_returnedAudioError];
478     if( !success )
479     {
480         msg_Err( p_demux, "audio output could not be added to capture session (%ld)", [o_returnedAudioError code] );
481         goto error;
482     }
483
484     [p_sys->session startRunning];
485
486     /* Set up p_demux */
487     p_demux->pf_demux = Demux;
488     p_demux->pf_control = Control;
489     p_demux->info.i_update = 0;
490     p_demux->info.i_title = 0;
491     p_demux->info.i_seekpoint = 0;
492
493     msg_Dbg( p_demux, "New audio es %d channels %dHz",
494             audiofmt.audio.i_channels, audiofmt.audio.i_rate );
495
496     p_sys->p_es_audio = es_out_Add( p_demux->out, &audiofmt );
497
498     [audioInput release];
499     [pool release];
500
501     msg_Dbg( p_demux, "QTSound: We have an audio device ready!" );
502
503     return VLC_SUCCESS;
504 error:
505     [audioInput release];
506     [pool release];
507
508     free( p_sys );
509
510     return VLC_EGENERIC;
511 }
512
513 /*****************************************************************************
514  * Close: destroy interface
515  *****************************************************************************/
516 static void Close( vlc_object_t *p_this )
517 {
518     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
519     demux_t *p_demux = (demux_t*)p_this;
520     demux_sys_t *p_sys = p_demux->p_sys;
521
522     [p_sys->session performSelectorOnMainThread:@selector(stopRunning) withObject:nil waitUntilDone:NO];
523     [p_sys->audiooutput performSelectorOnMainThread:@selector(release) withObject:nil waitUntilDone:NO];
524     [p_sys->session performSelectorOnMainThread:@selector(release) withObject:nil waitUntilDone:NO];
525
526     free( p_sys );
527
528     [pool release];
529 }
530
531 /*****************************************************************************
532  * Demux:
533  *****************************************************************************/
534 static int Demux( demux_t *p_demux )
535 {
536     demux_sys_t *p_sys = p_demux->p_sys;
537     block_t *p_blocka;
538     NSAutoreleasePool *pool;
539
540     p_blocka = block_New( p_demux, p_sys->i_audio_max_buffer_size );
541
542     if( !p_blocka )
543     {
544         msg_Err( p_demux, "cannot get audio block" );
545         return 0;
546     }
547
548     pool = [[NSAutoreleasePool alloc] init];
549
550     @synchronized (p_sys->audiooutput)
551     {
552         if ( [p_sys->audiooutput checkCurrentAudioBuffer] )
553         {
554             p_blocka->i_pts = [p_sys->audiooutput getCurrentPts];
555             p_blocka->p_buffer = [p_sys->audiooutput getCurrentAudioBufferData];
556             p_blocka->i_nb_samples = [p_sys->audiooutput getNumberOfSamples];
557             p_blocka->i_buffer = [p_sys->audiooutput getCurrentTotalDataSize];
558         }
559     }
560
561     if( !p_blocka->i_pts )
562     {
563         // Nothing to transfer yet, just forget
564         block_Release( p_blocka );
565         [pool release];
566         msleep( 10000 );
567         return 1;
568     }
569
570     [pool release];
571
572     if( p_blocka )
573     {
574         es_out_Control( p_demux->out, ES_OUT_SET_PCR, p_blocka->i_pts );
575         es_out_Send( p_demux->out, p_sys->p_es_audio, p_blocka );
576     }
577
578     return 1;
579 }
580
581 /*****************************************************************************
582  * Control:
583  *****************************************************************************/
584 static int Control( demux_t *p_demux, int i_query, va_list args )
585 {
586     bool *pb;
587     int64_t *pi64;
588
589     switch( i_query )
590     {
591             /* Special for access_demux */
592         case DEMUX_CAN_PAUSE:
593         case DEMUX_CAN_SEEK:
594         case DEMUX_SET_PAUSE_STATE:
595         case DEMUX_CAN_CONTROL_PACE:
596             pb = (bool*)va_arg( args, bool * );
597             *pb = false;
598             return VLC_SUCCESS;
599
600         case DEMUX_GET_PTS_DELAY:
601             pi64 = (int64_t*)va_arg( args, int64_t * );
602             *pi64 = INT64_C(1000) * var_InheritInteger( p_demux, "live-caching" );
603             return VLC_SUCCESS;
604
605         default:
606             return VLC_EGENERIC;
607     }
608     return VLC_EGENERIC;
609 }